优秀的编程知识分享平台

网站首页 > 技术文章 正文

C++ 智能指针线程安全:堆与栈的 "共享潜规则"

nanyue 2025-10-23 08:46:27 技术文章 2 ℃

C++ 智能指针的线程安全:堆与栈的 "室友守则"

一、先搞懂:栈和堆像什么?

想象你住的公寓楼:



  • 就是你自己的卧室,东西(变量)都是你的私产,别人(其他线程)进不来,操作自己的东西永远安全。
  • 就是公寓的公共厨房,所有住户(线程)都能进,这里的东西(动态分配的对象)谁都能碰,容易出乱子。



智能指针呢?它就像你在厨房放的 "共享工具":指针本身(比如shared_ptr<int> sp这个变量)在你的卧室(栈)里,是私有的;但它指向的工具(堆上的int对象)和 "借用登记本"(shared_ptr的控制块,记录有多少人在用这个工具)在公共厨房(堆)里,是共享的。

二、智能指针的线程安全:"登记本" 和 "工具" 要分开看

C++ 标准规定了智能指针的线程安全规则,简单说:



  1. 控制块(引用计数)是线程安全的:多个线程同时 "借" 或 "还" 工具(复制或销毁shared_ptr),登记本(引用计数)不会算错(原子操作保证)。
  2. 指向的对象(堆上数据)是线程不安全的:多个线程同时用这个工具(修改对象),会打架(数据竞争),得自己加锁。
  3. unique_ptr比较特殊:它是 "独用工具",不能复制只能转移,就像你的专属菜刀,从不让别人碰,所以只要别强行共享(比如传裸指针),就不会有线程安全问题。

三、案例说话:代码跑起来才明白

案例 1:shared_ptr的引用计数线程安全

场景:10 个线程同时复制同一个shared_ptr,最后看引用计数是否正确。



cpp

运行

#include <iostream>
#include <memory>
#include <thread>
#include <vector>

// 全局的共享指针,指向一个int(值不重要,看引用计数)
std::shared_ptr<int> global_sp = std::make_shared<int>(0);

// 线程函数:复制shared_ptr
void copy_shared_ptr() {
    // 每个线程复制一次全局shared_ptr,存在栈上(线程私有)
    std::shared_ptr<int> local_sp = global_sp;
    // 稍等一下,模拟实际工作
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
}

int main() {
    std::cout << "初始引用计数:" << global_sp.use_count() << std::endl; // 应该是1

    std::vector<std::thread> threads;
    // 创建10个线程
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(copy_shared_ptr);
    }

    // 等待所有线程结束
    for (auto& t : threads) {
        t.join();
    }

    std::cout << "所有线程结束后,引用计数:" << global_sp.use_count() << std::endl; // 应该还是1(因为local_sp都销毁了)
    return 0;
}



编译运行



bash

# 编译(需要链接线程库,-pthread)
g++ -std=c++11 -pthread shared_ptr_refcount.cpp -o refcount_demo

# 运行
./refcount_demo



预期输出



plaintext

初始引用计数:1
所有线程结束后,引用计数:1



解释:10 个线程各自复制了global_sp(引用计数临时涨到 11),但线程结束后局部的local_sp销毁(引用计数减回 1)。整个过程中引用计数的增减是原子操作,不会算错 —— 这就是控制块的线程安全。

案例 2:shared_ptr指向的对象线程不安全

场景:10 个线程同时用shared_ptr修改它指向的int,不加锁会出什么乱子?



cpp

运行

#include <iostream>
#include <memory>
#include <thread>
#include <vector>
#include <mutex>

// 共享的智能指针,指向一个计数器
std::shared_ptr<int> counter_sp = std::make_shared<int>(0);
// 准备一个锁(后面会用到)
std::mutex mtx;

// 线程函数:不加锁修改对象
void increment_without_lock() {
    for (int i = 0; i < 1000; ++i) {
        // 直接修改共享对象(危险!)
        (*counter_sp)++;
    }
}

// 线程函数:加锁修改对象
void increment_with_lock() {
    for (int i = 0; i < 1000; ++i) {
        // 加锁保护共享对象
        std::lock_guard<std::mutex> lock(mtx);
        (*counter_sp)++;
    }
}

int main() {
    // 测试不加锁的情况
    *counter_sp = 0; // 重置计数器
    std::vector<std::thread> threads1;
    for (int i = 0; i < 10; ++i) {
        threads1.emplace_back(increment_without_lock);
    }
    for (auto& t : threads1) {
        t.join();
    }
    std::cout << "不加锁,预期10000,实际:" << *counter_sp << std::endl;

    // 测试加锁的情况
    *counter_sp = 0; // 重置计数器
    std::vector<std::thread> threads2;
    for (int i = 0; i < 10; ++i) {
        threads2.emplace_back(increment_with_lock);
    }
    for (auto& t : threads2) {
        t.join();
    }
    std::cout << "加锁,预期10000,实际:" << *counter_sp << std::endl;

    return 0;
}



编译运行



bash

g++ -std=c++11 -pthread shared_ptr_obj.cpp -o obj_demo
./obj_demo



预期输出



plaintext

不加锁,预期10000,实际:9527(每次可能不同,小于10000)
加锁,预期10000,实际:10000



解释:不加锁时,多个线程同时读写*counter_sp,会出现 "读脏数据"(比如两个线程同时读到 5,都加 1 变成 6,实际应该是 7)。这说明:智能指针本身的引用计数安全,但它指的对象,该加锁还得加锁!

案例 3:unique_ptr的线程安全(别共享就没事)

场景:unique_ptr不能复制,只能通过移动转移所有权,多线程中安全传递。



cpp

运行

#include <iostream>
#include <memory>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>

// 用队列和条件变量安全传递unique_ptr
std::queue<std::unique_ptr<int>> q;
std::mutex q_mtx;
std::condition_variable cv;

// 生产者线程:创建unique_ptr并放入队列
void producer() {
    for (int i = 0; i < 5; ++i) {
        // 创建unique_ptr(在生产者线程的栈上)
        auto ptr = std::make_unique<int>(i);
        std::cout << "生产:" << *ptr << std::endl;
        
        // 加锁放入队列(转移所有权)
        std::lock_guard<std::mutex> lock(q_mtx);
        q.push(std::move(ptr)); // 必须用move,因为unique_ptr不能复制
    }
    // 通知消费者
    cv.notify_one();
}

// 消费者线程:从队列取unique_ptr并使用
void consumer() {
    std::unique_lock<std::mutex> lock(q_mtx);
    // 等待队列有数据
    cv.wait(lock, []{ return !q.empty(); });

    // 取出并使用
    while (!q.empty()) {
        auto ptr = std::move(q.front()); // 转移所有权到消费者线程
        q.pop();
        std::cout << "消费:" << *ptr << std::endl;
    }
}

int main() {
    std::thread prod(producer);
    std::thread cons(consumer);

    prod.join();
    cons.join();
    return 0;
}



编译运行



bash

g++ -std=c++11 -pthread unique_ptr_demo.cpp -o unique_demo
./unique_demo



预期输出



plaintext

生产:0
生产:1
生产:2
生产:3
生产:4
消费:0
消费:1
消费:2
消费:3
消费:4



解释:unique_ptr像 "一次性餐具",只能一个人用,用完传给下个人(通过std::move)。只要不强行复制(编译器会报错),多线程传递时就很安全 —— 毕竟 "独一份" 的东西,不会有人抢。

四、总结:智能指针的 "安全手册"

  1. shared_ptr的引用计数(控制块)是线程安全的,随便复制销毁不用慌。
  2. shared_ptr指向的对象,该加锁加锁,别指望智能指针帮你管对象的线程安全。
  3. unique_ptr只要不共享(别传裸指针给多个线程),天生线程安全,因为 "独占" 就是最好的保护。

标题

  1. 《C++ 智能指针线程安全:堆与栈的 "共享潜规则"》
  2. 《从厨房到卧室:用生活理解智能指针的线程安全》

简介

本文用生活化类比解析 C++ 智能指针的线程安全特性,从栈(线程私有)和堆(线程共享)的角度,说明shared_ptr的引用计数安全与指向对象的不安全,以及unique_ptr的独占式安全。通过多个可运行案例展示具体场景,帮助开发者轻松掌握智能指针在多线程中的正确用法。

关键词

#C++ 智能指针 #线程安全 #堆内存 #栈内存 #shared_ptr

最近发表
标签列表