C++ 并发与多线程学习笔记(七)原子操作

引例

先来设想一个场景:
存在一个变量,有线程要读这个变量,有线程要写这个变量:

int share_value = 0;
void threadFunc()
{
	for (int i = 0; i < 100000; i++) //每个线程进行10万次计算
		share_value++;
}
int main()
{
	clock_t start = clock();//为了分析性能,设置计时器来度量程序的效率
	thread thread1(threadFunc);
	thread thread2(threadFunc);
	thread1.join();
	thread2.join();

	cout << "变量值为:" << share_value << endl;
	clock_t end = clock();
	cout << "花费了" << (double)(end - start) << "毫秒" << endl;//输出计时
	cout << "主线程结束" << endl;
	return 0;
}

按理说,我们用两个线程各对这个变量加10万次,加起来应该是20万。我们来看看结果:
在这里插入图片描述
再运行一次:
在这里插入图片描述
可以看到,两次结果都没有20万。

计算结果因计算机CPU的性能而异,性能越强的CPU出现错误的几率更少,不排除根本不出错的现象,在不超出int值上限的情况下提高计算量都能更明显的差异。使用浮点数可能会有更大的误差。

学过操作系统我们知道,系统执行线程是有调度的,一定存在因为调度导致的问题。我们可以使用互斥锁来解决这个问题。修改线程入口函数如下:

int share_value = 0;
mutex my_mutex;
void threadFunc()
{
	for (int i = 0; i < 100000; i++)
	{
		my_mutex.lock();
		share_value++;
		my_mutex.unlock();
	}
}

运行结果:
在这里插入图片描述
可以看到计算结果已经正确了,现在我们再修改计算量比较加锁和不加锁的情况。
不加锁,计算量为2000万:
在这里插入图片描述
有时候也能计算正确,但是少数。
现在为加锁,计算量2000万:
在这里插入图片描述
不论运行多少次,加锁的操作都不会出现计算错误。但我们可以看到随着计算量的上涨,加锁带来的系统开销会成倍于计算量的增长量而变化,在面对大量的数据访问或者很长的执行语句的时候,加锁的弊端不得不考虑。

原子操作

概念:学过操作系统我们知道,原子操作的意义即为不可分割的操作。在多线程编程中,不会被打断的程序执行片段称为原子操作,在执行原子操作的时候,处理机不会调度到其他线程,所以当执行到原子操作的时候,要么执行直到原子操作完毕,要么不进入原子操作。从效率上来讲,原子操作比互斥锁要更胜一筹。
用法:

std::atomic<int> share_value = 0;

atomic是一个类模板,传入的模板类型即我们要对其操作的类型。原子操作的使用方式体现了它和互斥锁不同的应用场景,互斥锁用于几行代码的保护,原子操作用于变量的保护。
使用原子操作的方式保护数据,运行结果如下:
在这里插入图片描述
计算结果是正确的,虽然耗时也不少,但比使用互斥锁要优化了许多。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页