互斥 (Mutex: Mutual Exclusion)
C++11提供了4个互斥对象(C++14提供了1个)用于同步多个线程对共享资源的访问
互斥对象
类名 | 描述 |
---|---|
std::mutex | 最简单的互斥对象。 |
std::timed_mutex | 带有超时机制的互斥对象,允许等待一段时间或直到某个时间点仍未能获得互斥对象的所有权时放弃等待。 |
std::recursive_mutex | 允许被同一个线程递归的Lock和Unlock。 |
std::recursive_timed_mutex | 带超时机制的递归Lock |
std::shared_timed_mutex(C++14) | 允许多个线程共享所有权的互斥对象,如读写锁,本文不讨论这种互斥。 |
锁(lock\unlock)
互斥对象的主要操作:加锁(lock)和释放锁(unlock)。当一个线程对互斥对象进行lock操作并成功获得这个互斥对象的所有权,在此线程对此对象unlock前,其他线程对这个互斥对象的lock操作都会被阻塞。
头文件:< mutex >
类型: std::mutex
用法:在C++中,通过构造std::mutex的实例创建互斥元,调用成员函数lock()来锁定它,调用unlock()来解锁,不过一般不推荐这种做法,标准C++库提供了std::lock_guard类模板,实现了互斥元的RAII惯用语法。std::mutex和std::lock _ guard。都声明在< mutex >头文件中。
std::mutex mt;
...
mt.lock();
int_set.insert(dis(gen));
mt.unlock();
C++使用RAII进行自动资源管理(管理类模板)
C++通常使用RAII(Resource Acquisition Is Initialization)来自动管理资源。如果可能应总是使用标准库提供的互斥对象管理类模板。
互斥对象管理类模板 | 描述 |
---|---|
std::lock_guard | 严格基于作用域(scope-based)的锁管理类模板,构造时是否加锁是可选 的(不加锁时假定当前线程已经获得锁的所有权),析构时自动释放锁,所有权不可转移,对象生存期内不允许手动加锁和释放锁。 |
std::unique_lock | 更加灵活的锁管理类模板,构造时是否加锁是可选的 ,在对象析构时如果持有锁会自动释放锁,所有权可以转移。对象生命期内允许手动加锁和释放锁。 |
std::shared_lock(C++14) | 用于管理可转移和共享所有权 的互斥对象。 |
std::mutex mt;
...
std::lock_guard<std::mutex> lck(mt);
int_set.insert(dis(gen));
加锁策略
策略 | tag type | 描述 |
---|---|---|
(默认) | 无 | 请求锁,阻塞当前线程直到成功获得锁 。 |
std::defer_lock | std::defer_lock_t | 不请求锁。 |
std::try_to_lock | std::try_to_lock_t | 尝试请求锁,但不阻塞线程 ,锁不可用时也会立即返回。 |
std::adopt_lock | std::adopt_lock_t | 假定当前线程已经获得互斥对象的所有权 (所有权发生了转移?领养了锁),所以不再请求锁。 |
策略支持情况
策略 | std::lock_guard | std::unique_lock | std::shared_lock |
---|---|---|---|
(默认) | √ | √ | √(共享) |
std::defer_lock | × | √ | √ |
std::try_to_lock | × | √ | √ |
std::adopt_lock | √?还没搞明白 | √ | √ |
避免死锁
任意两个互斥对象,在多个线程中进行加锁时应保证其先后顺序是一致
std::mutex mt1, mt2;
// thread 1
{
std::lock_guard<std::mutex> lck1(mt1);
std::lock_guard<std::mutex> lck2(mt2);
// do something
}
// thread 2
{
std::lock_guard<std::mutex> lck2(mt2);
std::lock_guard<std::mutex> lck1(mt1);
// do something
}
标准库中的std::lock和std::try_lock函数来对多个Lockable对象加锁。std::lock(或std::try_lock)会使用一种避免死锁的算法对多个待加锁对象进行lock操作(std::try_lock进行try_lock操作),当待加锁的对象中有不可用对象时std::lock会阻塞当前线程知道所有对象都可用(std::try_lock不会阻塞线程当有对象不可用时会释放已经加锁的其他对象并立即返回)。
std::mutex mt1, mt2;
// thread 1
{
std::unique_lock<std::mutex> lck1(mt1, std::defer_lock);
std::unique_lock<std::mutex> lck2(mt2, std::defer_lock);
std::lock(lck1, lck2);
// do something
}
// thread 2
{
std::unique_lock<std::mutex> lck1(mt1, std::defer_lock);
std::unique_lock<std::mutex> lck2(mt2, std::defer_lock);
std::lock(lck2, lck1);
// do something
}
std::lock和std::try_lock还是异常安全的函数,当对多个对象加锁时,其中如果有某个对象在lock或try_lock时抛出异常,std::lock或std::try_lock会捕获这个异常并将之前已经加锁的对象逐个执行unlock操作,然后重新抛出这个异常(异常中立)。
线程间的锁
线程之间的锁有:互斥锁、条件锁、自旋锁、读写锁、递归锁。一般而言,锁的功能与性能成反比。
条件锁
条件锁就是所谓的条件变量,某一个线程因为某个条件为满足时可以使用条件变量使改程序处于阻塞状态。一旦条件满足以“信号量”的方式唤醒一个因为该条件而被阻塞的线程。最为常见就是在线程池中,起初没有任务时任务队列为空,此时线程池中的线程因为“任务队列为空”这个条件处于阻塞状态。一旦有任务进来,就会以信号量的方式唤醒一个线程来处理这个任务。
头文件:< condition_variable >
类型:std::condition_variable(只和std::mutex一起工作) 和 std::condition_variable_any(符合类似互斥元的最低标准的任何东西一起工作)。
//使用std::condition_variable等待数据
std::mutex mut;
std::queue<data_chunk> data_queue;
std::condition_variable data_cond;
void data_preparation_thread()
{
while(more_data_to_prepare())
{
data_chunk const data=prepare_data();
std::lock_guard<std::mutex> lk(mut);
data_queue.push(data);
data_cond.notify_one();
}
}
void data_processing_thread()
{
while(true)
{
std::unique_lock<std::mutex> lk(mut); //这里使用unique_lock是为了后面方便解锁
data_cond.wait(lk,{[]return !data_queue.empty();});
data_chunk data=data_queue.front();
data_queue.pop();
lk.unlock();
process(data);
if(is_last_chunk(data))
break;
}
}
wait()的实现接下来检查条件,并在满足时返回。如果条件不满足,wait()解锁互斥元,并将该线程置于阻塞或等待状态。当来自数据准备线程中对notify_one()的调用通知条件变量时,线程从睡眠状态中苏醒(解除其阻塞),重新获得互斥元上的锁,并再次检查条件,如果条件已经满足,就从wait()返回值,互斥元仍被锁定。如果条件不满足,该线程解锁互斥元,并恢复等待。
如果等待线程只打算等待一次,那么当条件为true时它就不会再等待这个条件变量了,条件变量未必是同步机制的最佳选择。如果等待的条件是一个特定数据块的可用性时,这尤其正确。在这个场景中,使用期值(future)更合适。使用future等待一次性事件。https://blog.csdn.net/xy_cpp/article/details/81910513
自旋锁
假设我们有一个两个处理器core1和core2计算机,现在在这台计算机上运行的程序中有两个线程:T1和T2分别在处理器core1和core2上运行,两个线程之间共享着一个资源。
``
互斥锁的工作原理,互斥锁是是一种sleep-waiting的锁。假设线程T1获取互斥锁并且正在core1上运行时,此时线程T2也想要获取互斥锁(pthread_mutex_lock),但是由于T1正在使用互斥锁使得T2被阻塞。当T2处于阻塞状态时,T2被放入到等待队列中去,处理器core2会去处理其他任务而不必一直等待(忙等)。也就是说处理器不会因为线程阻塞而空闲着,它去处理其他事务去了。
`
而自旋锁就不同了,自旋锁是一种busy-waiting的锁。也就是说,如果T1正在使用自旋锁,而T2也去申请这个自旋锁,此时T2肯定得不到这个自旋锁。与互斥锁相反的是,此时运行T2的处理器core2会一直不断地循环检查锁是否可用(自旋锁请求),直到获取到这个自旋锁为止。对比可以我们知道“自旋锁”是比较耗费CPU的。