多线程并发的竞态问题
我们创建三个线程同时进行购票,代码如下
#include<iostream>
#include<thread>
#include<list>
using namespace std;
//总票数
int ticketCount=100;
//售票线程
void sellTicket(int idx)
{
while(ticketCount>0)
{
cout<<ticketCount<<endl;
ticketCount--;
std::this_thread::sleep_for(std::chrono::milliseconds(100));//休眠100ms
}
}
int main()
{
list<std::thread> tlist;//存储线程
for(int i=1;i<=3;i++)//创建三个线程
{
tlist.push_back(std::thread(sellTicket,i));
}
for(auto& tl:tlist)
{
tl.join();//让主线程等待子线程执行结束
}
return 0;
}
我们再看这段代码的汇编过程
ticketCount--;
汇编代码如下:
mov eax,ticketCount
sub eax,1
mov ticketCount,eax
上述汇编过程的解读为:
- 将ticketCount的值从内存放到寄存器eax
- 通过寄存器完成减法操作
- 将运算结果再从eax寄存器中放到内存中
可以看到,三个线程在执行代码时,每个线程在执行到ticketCount--时,在底层都会执行上述三行汇编代码,这种竞态必然会导致最终结果的错误。
如:
- 假如现在ticketCount的值为100
- 线程一把ticketCount的值从内存放到寄存器并完成了减法操作,则此时ticketCount的值为99,但并未将计算后的结果放到内存,也就是说此时内存中ticketCount的值仍旧为100
- 线程二开始执行代码,那么线程二从内存取出ticketCount的值放到eax寄存器时必然为100,因此线程二在进行计算后的结果也是99
- 之后线程一又开始继续执行代码,将他的计算结果99写回内存,则此时输出结果为99
- 切换到线程二继续执行代码,然而线程二的结果也是99
可以看到,本来两个线程在执行减法操作后,ticketCount的结果应该为98,但是现在的结果却都是99。
出现上述结果的原因就在于ticketCount--代码执行的汇编过程不是一次性完成的
mutex互斥锁
这就是互斥锁出现的作用——保证ticketCount--代码的汇编过程一次性执行
std::mutex
是 C++11 引入的互斥量(Mutex)类,用于在多线程环境中实现互斥访问共享资源。通过
std::mutex
,可以确保在同一时间只有一个线程可以访问被保护的临界区,从而避免多个线程同时对共享数据进行修改而导致的数据竞争问题。
std::mutex
提供了lock()
和unlock()
方法,分别用于锁定和解锁互斥量。需要注意的是,在编写多线程程序时,必须确保每次lock()
操作都会有对应的unlock()
操作,以避免死锁等问题。
修改后的代码如下:
#include<iostream>
#include<thread>
#include<list>
#include<mutex>
using namespace std;
//总票数
int ticketCount=100;
std::mutex mtx;
//售票线程
void sellTicket(int idx)
{
while(ticketCount>0)
{
mtx.lock();//加锁
cout<<ticketCount<<endl;
ticketCount--;//解锁
mtx.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(100));//休眠100ms
}
}
int main()
{
list<std::thread> tlist;//存储线程
for(int i=1;i<=3;i++)//创建三个线程
{
tlist.push_back(std::thread(sellTicket,i));
}
for(auto& tl:tlist)
{
tl.join();//让主线程等待子线程执行结束
}
return 0;
}
但是上述代码仍旧有一些问题,考虑以下情况
- 假如ticketCount的值为1
- 由于ticketCount大于0,因此线程一进入while循环并获取锁,但并未执行--操作,因此此时ticketCount的仍旧为1
- 假如此时线程二刚好被切换,那么由于此时ticketCount的值还没有变化,仍旧为1大于0,因此线程二也进入while循环,但是线程一并未释放锁,因此线程将被卡住
- 之后线程一继续执行,执行减法操作,ticketCount的值为0,并释放锁
- 此时线程二继续执行,但是线程二已经进入while循环了,因此线程二也将执行一次减法操作,故而就会出现ticketCount=-1的情况
因此修正后的代码应该是
void sellTicket(int idx)
{
while(ticketCount>0)
{
mtx.lock();
if(ticketCount>0)
{
cout<<ticketCount<<endl;
ticketCount--;
}
mtx.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(100));//休眠100ms
}
}
lock_guard
由于mutex需要程序员时刻记住在何时加锁在何时释放锁,否则就会导致死锁问题,但大多数时候这个工作比较繁琐,并且很容易忘记释放锁,因此出现了lock_guard,可自动管理加锁和解锁
std::lock_guard
是 C++11 提供的 RAII(资源获取即初始化)风格的锁管理工具,用于自动管理std::mutex
的加锁和解锁操作。- 通过
std::lock_guard
,可以在作用域内自动锁定std::mutex
,并在作用域结束时自动释放锁,从而避免忘记手动解锁或异常情况下未能正确解锁互斥量。std::lock_guard
的构造函数接受一个std::mutex
对象,并在构造时锁定该互斥量,在析构时释放锁。因此,使用std::lock_guard
可以很方便地实现线程安全的代码块。
void sellTicket(int idx)
{
while(ticketCount>0)
{
// mtx.lock();
{
lock_guard<mutex> lock(mtx);
if(ticketCount>0)
{
cout<<ticketCount<<endl;
ticketCount--;
}
}
// mtx.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(100));//休眠100ms
}
}