ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【C++】线程库

【C++】线程库 目录一、基础使用1. 创建线程2. join() vs detach()3.this_thread 命名空间二、线程安全与同步1. 互斥锁mutex 普通互斥锁RAII 管理锁避免死锁lock_guardunique_locklock_guard 与 unique_lock 的区别recursive_mutex 递归互斥锁timed_mutex 超时互斥锁recursive_timed_mutex递归超时互斥锁自旋锁atomicCAS 无锁编程2、条件变量最简单的生产者-消费者核心 API 详解使用注意事项保证某个线程先运行锁 条件变量 flag一、基础使用1. 创建线程#include thread #include iostream void func(int x) { std::cout Thread: x std::endl; } int main() { std::thread t; // 创建一个线程但这个线程什么也不做 std::thread t1(func, 42); // 函数指针 std::thread t2([](int x) { // Lambda std::cout Lambda: x std::endl; }, 100); t1.join(); // 等待线程结束 t2.join(); return 0; }注意std::thread 的带参构造与普通类的带参构造不同std::thread 必须存储参数的副本因为线程可能在你调用 join() 之前执行父作用域传递的参数可能已经被销毁所以如果要在std::thread 的带参构造传递引用类型比如void modify(int x) { x 100; } int main() { int a 42; std::thread t(modify, a); // 传递引用 t.join(); std::cout a std::endl; return 0; }就要用std::ref明确告诉编译器我传递的是引用否则上面的代码会编译失败原因是类型不匹配下面是正确示例void modify(int x) { x 100; } int main() { int a 42; std::thread t(modify, ref(a)); // 传递引用 t.join(); std::cout a std::endl; return 0; }C thread 类不支持拷贝构造和拷贝赋值但支持移动构造和移动赋值。创建一批线程void func(int x) { std::cout Thread: x std::endl; } int main() { vectorthread threads(5); // 默认构造 for (int i 0; i 5; i) { threads[i] thread(func, 10); // 移动赋值 } for (int i 0; i 5; i) { threads[i].join(); } return 0; }2. join() vs detach()join()阻塞主线程等待子线程执行完毕推荐资源安全detach()分离线程让其在后台独立运行需确保线程不会访问已销毁的资源std::thread t(func); t.detach(); // 主线程不再等待t 不再代表任何线程重要线程对象析构前必须调用join()或detach()否则程序会std::terminate。3.this_thread 命名空间std::this_thread是 C11 引入的一个命名空间提供了当前线程的操作函数让线程能够控制自身的行为。namespace std::this_thread { // 获取当前线程 ID thread::id get_id() noexcept; // 让出 CPU 时间片 void yield() noexcept; // 暂停当前线程阻塞 templateclass Rep, class Period void sleep_for(const chrono::durationRep, Period sleep_duration); templateclass Clock, class Duration void sleep_until(const chrono::time_pointClock, Duration sleep_time); }二、线程安全与同步1. 互斥锁mutex 普通互斥锁int main() { // 两个线程同时共同对一个变量,有线程安全问题要加锁 int x 0; mutex mtx; int cnt1 0; int cnt2 0; cin cnt1 cnt2; thread t1([]() mutable { for (int i 0; i cnt1; i) { mtx.lock(); x; mtx.unlock(); } }); thread t2([]() mutable { for (int i 0; i cnt2; i) { mtx.lock(); x; mtx.unlock(); } }); t1.join(); t2.join(); std::cout x endl; return 0; }RAII 管理锁避免死锁上面的 t1 线程和 t2 线程只要有一个线程在 mtx.lock(); 和 mtx.unlock(); 之间出异常退出都会导致 mtx 死锁程序永远不会退出。使用 RAII 技术可以避免这个问题void func() { srand(time(0)); if (rand() % 2 0) { throw exception(异常); } else { cout func() endl; } } templatetypename T class my_lock_guard { public: lock_guard(T mtx):_mtx(mtx) { _mtx.lock(); } ~lock_guard() { _mtx.unlock(); } private: T _mtx; }; int main() { // 两个线程同时共同对一个变量,有线程安全问题要加锁 int x 0; mutex mtx; int cnt1 0; int cnt2 0; cin cnt1 cnt2; thread t1([]() mutable { try { for (int i 0; i cnt1; i) { my_lock_guardmutex lock(mtx); x; func(); // 模拟异常退出 } } catch (const exception e) { cout e.what() endl; } }); thread t2([]() mutable { for (int i 0; i cnt2; i) { mtx.lock(); x; mtx.unlock(); } }); t1.join(); t2.join(); std::cout x endl; return 0; }lock_guard{ std::lock_guardstd::mutex lock(mtx); // 构造函数自动加锁 // 临界区代码 // ... // 离开临界区调用析构函数自动解锁 }unique_lockstd::unique_lock是 C11 引入的互斥锁包装器相比std::lock_guard提供了更大的灵活性但代价是轻微的性能开销特性std::lock_guardstd::unique_lockRAII 自动加解锁✅✅延迟加锁❌✅手动解锁/重新加锁❌✅尝试加锁 (try_lock)❌✅条件变量配合❌ (不够灵活)✅ (必须)所有权转移❌✅ (移动语义)性能开销极小稍大维护状态标志构造方式#include mutex std::mutex mtx; // 1. 默认构造立即加锁最常用 std::unique_lockstd::mutex lock1(mtx); // 2. 延迟加锁构造时不锁后续手动锁 std::unique_lockstd::mutex lock2(mtx, std::defer_lock); lock2.lock(); // 手动加锁 // 3. 尝试加锁不阻塞 std::unique_lockstd::mutex lock3(mtx, std::try_to_lock); if (lock3.owns_lock()) { // 成功获得锁 } // 4. 假设已加锁接管已锁定的互斥量 std::unique_lockstd::mutex lock4(mtx, std::adopt_lock); // 此时 mtx 必须已被当前线程锁定核心成员函数std::unique_lockstd::mutex lock(mtx, std::defer_lock); lock.lock(); // 加锁阻塞 lock.try_lock(); // 尝试加锁非阻塞 lock.try_lock_for(duration);// 尝试加锁等待一段时间需支持 timed_mutex lock.try_lock_until(time); // 尝试加锁直到某时间点 lock.unlock(); // 手动解锁 lock.owns_lock() // 返回 bool当前是否拥有锁 lock.mutex() // 返回关联的互斥量指针 lock.operator bool()// 同 owns_lock() std::unique_lockstd::mutex lock1(mtx); std::unique_lockstd::mutex lock2 std::move(lock1); // 转移所有权 // lock1 不再拥有锁 std::mutex* ptr lock.release(); // 放弃所有权但互斥量仍处于锁定状态 // 需要手动 unlock ptr-unlock();lock_guard 与 unique_lock 的区别特性lock_guardunique_lock设计定位简单 RAII 包装器灵活锁管理器加锁时机构造时立即加锁可延迟、可提前手动解锁❌ 不支持✅ 支持重新加锁❌ 不支持✅ 支持try_lock❌ 不支持✅ 支持超时等待❌ 不支持✅ 支持配合 timed_mutex所有权转移❌ 不支持✅ 支持移动语义条件变量配合❌ 不支持✅必须配合性能开销零开销近乎有额外状态维护代码简洁性✅ 极简稍复杂recursive_mutex 递归互斥锁普通 mutex vs recursive_mutex#include mutex std::mutex m1; std::recursive_mutex m2; // 普通 mutex同一线程二次锁定 → 死锁 void test_normal() { m1.lock(); m1.lock(); // 死锁同一线程等待自己释放锁 m1.unlock(); m1.unlock(); } // recursive_mutex同一线程可多次锁定 void test_recursive() { m2.lock(); m2.lock(); // ✅ 允许锁计数变为 2 m2.unlock(); // 计数 1 m2.unlock(); // 计数 0真正释放 }工作原理维护一个锁定计数(lock count)同一线程每次lock()计数 1每次unlock()计数 -1只有当计数归零时其他线程才能获取锁timed_mutex 超时互斥锁std::timed_mutex是 C11 引入的超时互斥量在普通互斥量基础上增加了超时等待功能允许线程在指定时间内尝试获取锁避免无限阻塞。// 普通 mutex只有阻塞和非阻塞 std::mutex mtx; mtx.lock(); // 阻塞直到获取 mtx.try_lock(); // 立即返回 bool // timed_mutex增加了超时功能 std::timed_mutex tmtx; tmtx.lock(); // 阻塞直到获取 tmtx.try_lock(); // 立即返回 bool tmtx.try_lock_for(duration); // 等待一段时间 tmtx.try_lock_until(time_point); // 等到某个时间点try_lock_for() - 等待一段时间#include mutex #include chrono #include thread std::timed_mutex tmtx; void worker(int id) { // 尝试在 100ms 内获取锁 if (tmtx.try_lock_for(std::chrono::milliseconds(100))) { std::cout Thread id acquired lock std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(50)); tmtx.unlock(); } else { std::cout Thread id timeout, giving up std::endl; } }try_lock_until() - 等到指定时间点void worker_until(int id) { auto deadline std::chrono::steady_clock::now() std::chrono::milliseconds(100); if (tmtx.try_lock_until(deadline)) { std::cout Thread id acquired lock before deadline std::endl; tmtx.unlock(); } else { std::cout Thread id missed deadline std::endl; } }recursive_timed_mutex递归超时互斥锁std::recursive_timed_mutex是 C11 引入的递归超时互斥量结合了recursive_mutex的递归特性和timed_mutex的超时功能是 C 标准库中最强大的互斥量类型。自旋锁在 mutex 普通互斥锁的两个线程对一个变量的例子中临界区代码只有一行对变量的操作如果采用普通互斥锁会有较大的线程上下文切换开销线程刚进入睡眠状态其他线程就把临界区代码执行完了降低了效率。这种临界区代码很短的情况适合使用自旋锁自旋锁如果锁被占用当前线程不会睡眠而是原地忙等一直循环检测锁状态。如果使用自旋锁又对 cpu 的占用太高了。解决方式可以是使用下面介绍的原子变量atomicstd::atomic是 C11 引入的原子操作库提供了无锁lock-free的线程安全数据类型是实现高性能并发编程的关键工具。什么是原子操作// ❌ 非原子操作可能被中断 int counter 0; counter; // 实际是三步操作 // 1. 读取 counter 到寄存器 // 2. 寄存器 1 // 3. 写回 counter // 多个线程同时执行时数据可能不一致 // ✅ 原子操作不可中断 std::atomicint counter{0}; counter; // 一步完成线程安全原子性保证非原子操作可能出错 线程A: 读取(0) → 计算(1) → 写回(1) 线程B: 读取(0) → 计算(1) → 写回(1) 结果counter 1丢失一次递增 原子操作正确 线程A: 读取(0) → 计算(1) → 写回(1) [不可中断] 线程B: 读取(1) → 计算(2) → 写回(2) 结果counter 2正确基本用法1. 创建原子变量#include atomic // 基本类型 std::atomicint ai{0}; // 初始化为 0 std::atomicbool flag{false}; std::atomiclong long big{0}; // 指针类型 int value 42; std::atomicint* ptr{value}; // 自定义类型必须是 trivially copyable struct Point { int x, y; }; std::atomicPoint p{Point{1, 2}};2. 基本操作std::atomicint counter{0}; // 存储和加载 counter.store(10); // 写入 int value counter.load(); // 读取 // 交换 int old counter.exchange(20); // 交换并返回旧值 // 比较并交换CAS int expected 20; bool success counter.compare_exchange_strong(expected, 30); // 如果 counter 20设置为 30返回 true // 否则 expected 被设置为 counter 的当前值返回 false // 算术操作 counter; // 后置 counter; // 前置 counter 5; // 加 counter.fetch_add(3); // 加并返回旧值 counter.fetch_sub(2); // 减并返回旧值3、应用举例在 mutex 普通互斥锁的两个线程对一个变量的例子中我们就可以将这个变量定义为原子变量使得它的操作是原子的。int main() { // 两个线程同时共同对一个变量,有线程安全问题将这个变量设置为原子变量 atomicint x 0; mutex mtx; int cnt1 10000; int cnt2 10000; thread t1([]() mutable { for (int i 0; i cnt1; i) { //mtx.lock(); x; //mtx.unlock(); } }); thread t2([]() mutable { for (int i 0; i cnt2; i) { //mtx.lock(); x; //mtx.unlock(); } }); t1.join(); t2.join(); std::cout x endl; return 0; }对于智能指针 shared_ptr如果两个线程同时调用智能指针的拷贝构造用智能指针指向同一个对象也会有线程安全问题因为涉及到对同一个引用计数的操作有了上面的原子变量类我们就可以将智能指针的引用计数设置为原子变量。CAS 无锁编程AS全称Compare-And-Swap比较并交换是一条CPU原子指令。它的工作模式像一个乐观的“检查-执行”事务“我认为内存地址V当前的值应该是A。如果是我就把它更新为B如果不是我什么都不做并告诉我失败了。”V要操作的内存位置变量A预期的旧值期望值B要设置的新值更新值这个操作由硬件保证是原子的不可中断。因此我们利用它来实现“无锁”的并发更新避免了线程阻塞和上下文切换的开销。上面的原子变量实现对变量操作的原子性就是基于 CAS 指令完成的。对变量操作的汇编代码mov eax,dword ptr [x] add eax,1 mov dword ptr [x],eax我们定义 CAS(VAB)V、A、B 的含义如上修改上面的汇编代码如下do { mov old,dword ptr [x] // 存储旧值 mov eax,dword ptr [x] // eax 存储修改后的值 add eax,1 }while(!CAS(x,old,eax));就可以实现对变量的原子操作读者可以分析两个线程分别执行以上汇编代码时的各种情况可以发现是线程安全的并且全程没有加锁如果一个线程对变量失败了顶多多执行几条cpu指令。除了对变量的无锁编程以外还有对链表尾插的无锁编程EnQueue(Q,data) { n new node(); n-value data; n-next NULL; do { p Q-tail; // 取链表尾指针的快照 }while(!CAS(p-next,NULL,n)); CAS(Q-tail,p,n); // 更新尾结点 }C11引入了std::atomicT模板类其中最关键的两个CAS成员函数是compare_exchange_weak弱比较交换compare_exchange_strong强比较交换bool compare_exchange_strong(T expected, T desired, memory_order order memory_order_seq_cst); bool compare_exchange_weak(T expected, T desired, memory_order order memory_order_seq_cst);2、条件变量std::condition_variable是 C 线程同步的核心工具用于实现线程间的事件通知和等待/唤醒机制。最简单的生产者-消费者#include iostream #include thread #include mutex #include condition_variable #include queue std::mutex mtx; std::condition_variable cv; std::queueint queue; const int MAX_SIZE 10; bool done false; // 生产者 void producer() { for (int i 0; i 20; i) { std::unique_lockstd::mutex lock(mtx); // 等待队列未满 cv.wait(lock, []{ return queue.size() MAX_SIZE; }); queue.push(i); std::cout Produced: i , queue size: queue.size() std::endl; // 通知消费者 cv.notify_one(); } // 生产完成 std::lock_guardstd::mutex lock(mtx); done true; cv.notify_all(); // 唤醒所有消费者 } // 消费者 void consumer(int id) { while (true) { std::unique_lockstd::mutex lock(mtx); // 等待队列非空或生产完成 cv.wait(lock, []{ return !queue.empty() || done; }); if (queue.empty() done) { std::cout Consumer id exiting std::endl; break; } int value queue.front(); queue.pop(); std::cout Consumer id consumed: value std::endl; cv.notify_one(); // 通知生产者 } } int main() { std::thread prod(producer); std::thread cons1(consumer, 1); std::thread cons2(consumer, 2); prod.join(); cons1.join(); cons2.join(); return 0; }核心 API 详解wait() 系列// 基础版本无谓词 void wait(std::unique_lockstd::mutex lock); // 行为 // 1. 原子地释放锁并进入等待 // 2. 被唤醒后重新获取锁 // 3. 可能会被虚假唤醒 // 推荐版本带谓词 templatetypename Predicate void wait(std::unique_lockstd::mutex lock, Predicate pred); // 行为 // 1. 检查 pred()如果为 true立即返回 // 2. 如果为 false释放锁并等待 // 3. 被唤醒后重新获取锁再次检查 pred() // 4. 直到 pred() 为 true 才返回 // 超时版本 templatetypename Predicate bool wait_for(std::unique_lockstd::mutex lock, const std::chrono::duration timeout, Predicate pred); templatetypename Predicate bool wait_until(std::unique_lockstd::mutex lock, const std::chrono::time_point timeout, Predicate pred);notify() 系列// 唤醒一个等待线程如果存在 void notify_one() noexcept; // 注意不保证唤醒哪个线程 // 唤醒所有等待线程 void notify_all() noexcept; // 所有线程被唤醒竞争获取锁使用注意事项1. 必须使用 unique_lock因为要支持手动解锁// ❌ 错误lock_guard 不能用于条件变量 std::lock_guardstd::mutex lock(mtx); cv.wait(lock); // 编译错误 // ✅ 正确必须使用 unique_lock std::unique_lockstd::mutex lock(mtx); cv.wait(lock);2. 必须使用谓词检查条件// ❌ 危险可能虚假唤醒 cv.wait(lock); if (condition) { // 可能因为虚假唤醒而条件不满足 } // ✅ 安全使用谓词 cv.wait(lock, []{ return condition; }); // 自动处理虚假唤醒3. 锁的持有时间// ❌ 错误持锁通知 void bad_notify() { std::unique_lockstd::mutex lock(mtx); // 修改数据 cv.notify_one(); // 锁一直持有到函数结束 // 等待线程被唤醒后无法立即获取锁 } // ✅ 正确解锁后通知 void good_notify() { { std::lock_guardstd::mutex lock(mtx); // 修改数据 } // 锁释放 cv.notify_one(); // 通知时锁已释放 } // ✅ 或者使用 unique_lock 提前解锁 void good_notify_v2() { std::unique_lockstd::mutex lock(mtx); // 修改数据 lock.unlock(); // 提前释放 cv.notify_one(); }4. 虚假唤醒Spurious Wakeup// 操作系统可能在没有通知的情况下唤醒线程 // 这就是为什么必须使用谓词或循环检查 // 手动处理版本 while (!condition) { cv.wait(lock); } // 使用谓词版本推荐 cv.wait(lock, []{ return condition; }); // 内部实现就是上面的循环保证某个线程先运行锁 条件变量 flag题目有两个线程 t1 和 t2让 t1 和 t2 交替打印t1 打印奇数t2 打印偶数并且保证 t1 先打印解决方案互斥锁 条件变量 flag 标志控制int main() { mutex mtx; condition_variable cv; bool flag false; int x 1; thread t1([]() { for (int i 0; i 5; i) { unique_lockmutex lock(mtx); while (flag) cv.wait(lock); cout t1: x endl; x; flag true; lock.unlock(); cv.notify_one(); } }); thread t2([]() { for (int i 0; i 5; i) { unique_lockmutex lock(mtx); while (!flag) cv.wait(lock); cout t2: x endl; x; flag false; lock.unlock(); cv.notify_one(); } }); t1.join(); t2.join(); return 0; }
返回列表