1. 单线程(单窗口)卖票
static int Count = 100;
void SellTicket() {
while (Count > 0) {
Count--;
}
}
如果在单线程环境下,该方法永远不会出错,因为不会产生竞态条件,那么什么是竞态条件。
2. 竞态条件
多线程环境中,无论线程如何随着调度算法的不同产生不同的执行顺序,运行结果总能保持一致性。
3.1 第一个多线程版本:锁加到循环外边
static int Count = 100;
std::mutex tex;
void SellTicket(int threadid) {
tex.lock();
while (Count > 0) {
Count--;
cout << "threadid = " << threadid << "卖出一张票" << ",目前剩余" << Count << "张票" << endl;
}
tex.unlock();
}
int main() {
list<thread>tlist;
for (int i = 0; i < 3;++i) {
tlist.push_back(std::thread(SellTicket,i));
}
for (thread& t : tlist) {
t.join();
}
return 0;
}
过程分析:
当一个线程获取到锁以后,其他线程就无法进入到循环中,所以有一个很严重的问题,一旦某个线程获取锁进入循环,会将所有票卖出以后,才会退出循环并释放锁,那么其他线程获取锁后就没有票可卖,设计很不合理。
执行结果:
3.2 第二个多线程版本:锁放在循环中
void SellTicket(int threadid) {
while (Count > 0) {
tex.lock();
Count--;
cout << "threadid = " << threadid << "卖出一张票" << ",目前剩余" << Count << "张票" << endl;
tex.unlock();
}
}
过程分析:
我们知道Count--;
并非是一个原子性的操作,在汇编层面至少有三条指令:
mov eax, Count
sub eax,1
move Count,eax
当我们的Count == 1
时,线程A进入循环并且获取到锁,执行上面三条指令的第一条以后,时间片到了,此时Count == 1
,还没有被改变,那么其他两个线程BC也可以进入到循环中,只不过无法获取锁,被阻塞;
然后,线程A指向完上面的三条指令,并且将锁释放
最后,线程BC以某种顺序依次获取锁,并且对Count做减1操作,那么Count的最终值为-2,也就是说已经没票了,但还是被卖了两次。
执行结果:
3.3 第三个多线程版本:锁+双重判断解决版本2存在的问题
void SellTicket(int threadid) {
while (Count > 0) { //第一层判断
tex.lock();
if (Count > 0) { //第二层判断
Count--;
cout << "threadid = " << threadid << "卖出一张票" << ",目前剩余" << Count << "张票" << endl;
}
tex.unlock();
}
}
过程分析:
根据版本2存在的问题,我们添加了第二层判断,线程A释放锁之后,其他线程BC某个线程获取锁以后,分别判断Count的值是大于0的,线程BC直接解锁走人。
执行结果:
3.4 第四个多线程版本:解决unlock()可能无法被正常调用导致的情况,见代码
void SellTicket(int threadid) {
while (Count > 0) {
tex.lock();
if (Count > 0) {
Count--;
/*
实际开发中,这里的可能会有其他的代码,这些代码可能导致程序异常退出、返回、抛异常,
导致tex.unlock(); 不被执行到,那么这把锁将永远不会被其他线程获取,一直阻塞在这里
*/
cout << "threadid = " << threadid << "卖出一张票" << ",目前剩余" << Count << "张票" << endl;
}
tex.unlock();
}
}
C++11引入lock_guard
或者unique_lock
解决上述问题,lock_guard
或者unique_lock
对象被创建时会自动上锁,出作用域会或者被析构时,会在析构函数自动解锁。
void SellTicket(int threadid) {
while (Count > 0) {
{
std::lock_guard<std::mutex> lk(tex);
//std::unique_lock<std::mutex> lk(tex);
if (Count > 0) {
Count--;
cout << "threadid = " << threadid << "卖出一张票" << ",目前剩余" << Count << "张票" << endl;
}
}//出该临时作用域,自动解锁
}
}
执行结果:
3.5 第五个多线程版本:volatile 关键字解决线程缓存共享数据导致数据读写不一致的问题
volatile static int Count = 100;
关于volatile关键字的作用,可以在网上搜一下,也可以看一下我的这篇文章:C++关键字之volatile
4.总结
实际上,对于简单类型的互斥操作使用原子变量即可,这里主要是总结一下我们在进行多线程互斥操作时需要考虑的问题。