C++11 多线程 - Part 4: 数据共享和争用条件
翻译自:https://thispointer.com//c11-multithreading-part-4-data-sharing-and-race-conditions/
在多线程环境中,线程间的数据共享非常容易。但是这种简单的数据共享可能会造成应用程序出现问题。其中一个问题就是争用条件。
什么是争用条件
争用条件是一种发生在多线程应用程序中的bug
当两个或多个线程同时执行一系列的操作时,这些操作访问相同的内存位置。此外,他们其中的一个或多个线程修改该内存位置中的数据,这种有时会导致意外的结果。
这就叫作争用条件。
争用条件通常很难发现和再现,因为他们不是每次都会发生。
只有当两个或多个线程执行操作的相对顺序导致意外结果时,才会发生这种情况。让我们通过一个例子来理解,
争用条件的一个实例
让我们创建一个Wallet类,它在内部维护money并提供一个服务/功能,即addMoney()。此成员函数按指定的计数递增wallet对象的内部money。
class Wallet
{
int mMoney; // 默认是private
public:
Wallet() :mMoney(0){}
int getMoney() { return mMoney; }
void addMoney(int money)
{
for(int i = 0; i < money; ++i)
{
mMoney++;
}
}
};
现在让我们创建5个线程,所有这些线程将共享一个Wallet类的相同对象,并使用其addMoney()成员函数并行向内部money添加1000个。
所以,如果最初钱包里的钱是0。完成所有线程的执行后,钱包里的钱应该是5000。
但是,由于所有线程都在同时修改共享数据,在某些情况下,最终钱包中的钱可能远低于5000。
我们来测试一下,
#include<iostream>
#include<thread>
#include <vector>
class Wallet
{
int mMoney; // 默认是private
public:
Wallet() :mMoney(0){}
int getMoney() { return mMoney; }
void addMoney(int money)
{
for(int i = 0; i < money; ++i)
{
mMoney++;
}
}
};
int testMultithreadedWallet()
{
Wallet walletObject;
std::vector<std::thread> threads;
for(int i = 0; i < 5; ++i){
threads.push_back(std::thread(&Wallet::addMoney, &walletObject, 1000));
}
for(int i = 0; i < threads.size() ; i++)
{
threads.at(i).join();
}
return walletObject.getMoney();
}
int main()
{
int val = 0;
for(int k = 0; k < 1000; k++)
{
if((val = testMultithreadedWallet()) != 5000)
{
std::cout << "Error at count = "<<k<<" Money in Wallet = "<<val << std::endl;
}
}
return 0;
}
由于同一Wallet类对象的addMoney() 成员函数执行了5次,因此它的内部货币预计为5000。但由于addMoney()成员函数是并行执行的,因此在某些情况下,mMoney将比5000小得多,即:
/*输出结果:
Error at count = 0 Money in Wallet = 4000
Error at count = 3 Money in Wallet = 3603
Error at count = 7 Money in Wallet = 3695
…
Error at count = 925 Money in Wallet = 4694
Error at count = 928 Money in Wallet = 4591
*/
这是一个争用条件,因为这里两个或多个线程同时试图修改同一内存位置,并且导致了意外的结果。
为什么发生这种情况
每个线程并行地递增同一“mMoney”成员变量。虽然看起来只有一行,但是这个“mMoney++”实际上被转换成了三个机器命令,
* 在寄存器中加载“mMoney”变量值
* 递增寄存器的值
* 用寄存器的值更新变量“mMoney”
现在假设在一个特殊的场景中,上面这些命令的执行顺序如下,

在这种情况下,一个增量将被忽略,因为不同的寄存器将被递增,而“mMoney”变量的值将被覆盖,而不是将“mMoney”变量递增两次。
假设在此场景之前,mMoney是46,如上图所示,它增加了2次,所以预期结果是48。但由于上述场景中的争用条件,最终的mMoney将仅为47。
这被称为争用条件。
如何解决争用条件
为了解决这个问题,我们需要使用锁机制,即在修改或读取共享数据之前,每个线程都需要获取一个锁,在修改数据之后,每个线程都应该解锁该锁。
我们将在下一篇文章中讨论这个问题。