使用C++11多线程模拟多窗口售票出现的常见问题分析总结以及最终解决方案

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;
}

过程分析:

当一个线程获取到锁以后,其他线程就无法进入到循环中,所以有一个很严重的问题,一旦某个线程获取锁进入循环,会将所有票卖出以后,才会退出循环并释放锁,那么其他线程获取锁后就没有票可卖,设计很不合理。

执行结果:
使用C++11多线程模拟多窗口售票出现的常见问题分析总结以及最终解决方案_第1张图片

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,也就是说已经没票了,但还是被卖了两次。

执行结果:
使用C++11多线程模拟多窗口售票出现的常见问题分析总结以及最终解决方案_第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直接解锁走人。

执行结果:
使用C++11多线程模拟多窗口售票出现的常见问题分析总结以及最终解决方案_第3张图片

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 lk(tex);
			if (Count > 0) {
				Count--;
				cout << "threadid = " << threadid << "卖出一张票" << ",目前剩余" << Count << "张票" << endl;
			}
		}//出该临时作用域,自动解锁
	}
}

执行结果:
使用C++11多线程模拟多窗口售票出现的常见问题分析总结以及最终解决方案_第4张图片

3.5 第五个多线程版本:volatile 关键字解决线程缓存共享数据导致数据读写不一致的问题

volatile static int Count = 100;

关于volatile关键字的作用,可以在网上搜一下,也可以看一下我的这篇文章:C++关键字之volatile

4.总结

实际上,对于简单类型的互斥操作使用原子变量即可,这里主要是总结一下我们在进行多线程互斥操作时需要考虑的问题。

你可能感兴趣的:(C++11常用特性,c++11多线程,c++,多线程)