线程安全问题
C++中各种类型的线程安全问题
在C++多线程编程中,主要有以下几类线程安全问题:
- 数据竞争 - 当多个线程同时访问并试图修改同一处数据时,由于执行顺序不确定造成的数据不一致、数据损坏等问题。
- 死锁 - 多个线程相互持有对方需要的锁资源而造成的死循环等待。
- 饥饿 - 一个线程由于优先级太低,始终得不到CPU调度执行而造成的“饿死”。
- 优先级反转 - 高优先级的线程因等待低优先级线程持有的资源而长时间阻塞。
- 原子操作失败 - 无法保证一个操作的完整性和原子性。
- order 竞争 - 多个线程对同一数据的顺序访问存在不确定性。
主要的解决方法包括:互斥量、信号量、事件、原子操作、线程同步和通信等。
提前设计,注重线程安全,采用合理的同步原语。
解决线程安全问题的多种方案
互斥量(Mutex)
使用互斥量锁保护共享数据的访问,确保同一时刻只有一个线程可以访问该数据:
1 | std::mutex mtx; |
信号量(Semaphore)
控制同时访问共享资源的线程数量
1 | std::semaphore sem(3); |
原子操作(Atomic)
将复合操作作为一个不可分割的原子操作执行
C++中利用原子操作(Atomic Operations)来实现线程安全的一个典型例子是原子计数器:
1 | #include <iostream> |
编译及输出结果
1 | 编译指令如下 |
分析:
- 使用std::atomic保证对共享counter的原子操作
- 在主线程中加入t1.join()和t2.join(),确保两个工作线程结束
- 之后再读取counter的值
这样可以确保计数计算的正确性。
std::atomic和std::thread提供了更简洁合理的多线程控制方法。
相比直接使用pthread来说,C++11的多线程库可以帮助我们更轻松地编写线程安全和高效的多线程程序。
线程同步(Synchronization)
使用条件变量实现线程间的同步配合
1 | #include <iostream> |
编译及输出结果
1 | 编译指令如下 |
分析
- mutex 互斥锁,保护共享数据
- condition_variable 条件变量,线程间同步
- unique_lock/lock_guard 上锁
- wait 等待条件满足
- notify_one 条件通知
通过这些同步原语,可以安全地在线程间通信和同步操作。
condition_variable的灵活性比简单的join要高,可以指定任意同步条件。
这就是一个使用C++标准线程库进行线程同步和通信的示例。
std::condition_variable::notify_one()是什么
std::condition_variable::notify_one()是C++线程编程中一个重要的同步原语。
它的作用是通知一个正在等待该条件变量的线程。
当使用std::condition_variable进行线程同步时,使用步骤一般是:
- 一个或多个线程等待某个条件,通过cv.wait()挂起自身。
- 另一个线程在条件成立时,通过cv.notify_one()通知一个等待的线程。
- 被通知的线程会从wait()函数返回,然后检查条件并继续运行。
例如:
1 | std::mutex mtx; |
通过notify_one()通知,可以唤醒一个等待该条件的线程,让其检查conditions并继续运行。
notify_all() variant 会通知所有等待线程。
所以std::condition_variable::notify_one()是线程通信的一个重要手段,可以实现复杂的线程同步。
std::condition_variable是C++中进行线程同步和通信的一个重要工具,常见的用法有:
等待条件
1
2
3
4std::mutex mtx;
std::condition_variable cv;
cv.wait(lock, []{return ready;}); // 等待ready为true用wait()挂起线程,直到给定的条件为真。
通知一个线程
1
cv.notify_one(); // 唤醒一个线程
notify_one()会唤醒一个等待该条件的线程。
通知所有线程
1
cv.notify_all(); // 唤醒所有线程
notify_all()会通知所有等待在该条件变量的线程。
设置超时时间
1
cv.wait_for(lock, std::chrono::seconds(1), []{return ready;});
wait_for()可以设置超时时间,在指定时间后会自动返回。
捕捉等待期间的异常
1
cv.wait(lock, []{return ready;}, [&]{handleError();});
通过传递异常处理器,可以处理wait()期间抛出的异常。
综上,condition_variable通过灵活的等待策略和通知机制,可以帮助实现强大的多线程同步。
unique_lock/lock_guard 上锁 是什么
std::unique_lock和std::lock_guard都是C++中实现互斥锁的RAII类。
std::unique_lock实现了移动语义和所有权的独占锁。主要功能有:
- 构造时自动上锁
- 支持移动语义(移动构造函数和移动赋值操作符)
- release()可以手动释放锁
- 支持定时上锁try_lock_for()和try_lock_until()
- 支持条件变量wait(),允许在等待时解锁
- 析构时自动解锁
所以unique_lock功能非常全面和灵活。
std::lock_guard是一个非拷贝非移动的简单锁封装类。主要用于通过RAII保证互斥锁一定会被释放。主要功能是:
- 构造时自动上锁
- 不允许拷贝构造和移动构造
- 析构时自动解锁
lock_guard不能手动解锁,也不能和条件变量一起使用。但代码量少,通常用在简单的局部锁场景。
两者都是通过RAII来保证加锁和解锁成对执行,避免死锁。
unique_lock功能更全面,lock_guard使用更简单。