在线程间共享数据
参考博客
共享数据的问题
设想你有一段时间和朋友合租公寓,公寓只有一个厨房和一个浴室。除非你们的感情格外深厚,否则不可能同时使用浴室。另外,假若朋友占用浴室很久,而你恰好也需要,便会感到不方便。类似地,假设你们使用的是组合烤箱,尽管可以同时烹饪,但若一人要烤香肠,同时另一人却要烘蛋糕,结果应该不会太好。并且,我们也清楚共用办公空间的烦恼:事情还没做完,有人却借走了工作所需之物,或者半成品被别人擅自更改
线程亦如此。若在线程之间共享数据,我们需要遵循规范:具体哪个线程按何种方式访问什么数据;还有,一旦改动了数据,如果牵涉到其他线程,它们要在何时以什么通信方式获得通知。同一进程内的多个线程之间,虽然可以简单易行地共享数据,但这不是绝对的优势,有时甚至是很大的劣势。不正确地使用共享数据,是产生与并发有关的错误的一个很大的诱因,其后果远比“香肠口味的蛋糕”严重
恶性条件竞争
诱发恶性条件竞争的典型场景是,要完成一项操作,却需改动两份或多份不同的数据,如上例中的两个链接指针。因为操作涉及两份独立的数据,而它们只能用单独的指令改动,当其中一份数据完成改动时,别的线程有可能不期而访
。因为满足条件的时间窗口短小,所以条件竞争往往既难察觉又难复现。若改动操作是由连续不间断的CPU指令完成的,就不太有机会在任何的单次运行中引发问题,即使其他线程正在并发访问数据。只有按某些次序执行指令才可能引发问题。随着系统负载加重及执行操作的次数增多,这种次序出现的机会也将增加。“屋漏偏逢连夜雨”几乎难以避免,且这些问题偏偏会在最不合时宜的情况下出现。恶性条件竞争普遍“挑剔”出现的时机,当应用程序在调试环境下运行时,它们常常会完全消失,因为调试工具影响了程序的内部执行时序,哪怕只影响一点点
使用互斥量保护共享数据
在访问共享数据前,开发者可使用互斥量将相关数据锁住,并于访问结束后将数据解锁。因此,线程库需要保证当一个线程使用特定互斥量锁住共享数据时,其他线程仅可在数据被解锁后才能访问
lock()、 unlock()上锁解锁
C++中通过实例化std::mutex创建互斥量,通过调用成员函数lock()进行上锁,unlock()进行解锁
在实际中必须成对使用,一旦在函数中用了lock,则在函数出口处必须调用unlock
mutex my_mutex;
int a = 1;
bool func()
{
my_mutex.lock();
if(!a)
{
cout << "a = " << a << endl;
my_mutex.unlock();
return false;
}
my_mutex.unlock();
return true;
}
注意上述代码在return前均调用了unlock
RAII std::lock_guard
C++标准库为互斥量提供了一个RAII语法的模板类std::lock_guard,其会在**构造的时候提供已锁的互斥量,并在析构的时候进行解锁
**,从而保证了一个已锁的互斥量总是会被正确的解锁
mutex my_mutex;
int a = 1;
bool func()
{
lock_guard<mutex> my_guard(my_mutex);
// my_mutex.lock();
if(!a)
{
cout << "a = " << a << endl;
// my_mutex.unlock();
return false;
}
// my_mutex.unlock();
return true;
}
可以通过限制lock_guard的作用域来提前释放锁
mutex my_mutex;
int a = 1;
bool func()
{
// lock_guard<mutex> my_guard(my_mutex);
// my_mutex.lock();
if (!a)
{
{
lock_guard<mutex> my_guard(my_mutex);
cout << "a = " << a << endl;