多线程:原子操作解决线程冲突

转载自C++拾遗–多线程:原子操作解决线程冲突

前言

  • 在多线程中操作全局变量一般都会引起线程冲突

1 线程冲突

#include <stdio.h>
#include <stdlib.h>
#include <process.h>
#include <Windows.h>
int g_count = 0;
void count(void *p)
{
	Sleep(100);    //do some work
	//每个线程把g_count加1共10次
	for (int i = 0; i < 10; i++)
	{
		g_count++;
	}
	Sleep(100);   //do some work
}
int main(void)
{
	printf("******多线程访问全局变量演示***by David***\n");
	//共创建10个线程
	HANDLE handles[10];
	for (int i = 0; i < 10; i++)
	{
		for (int j = 0; j < 10; j++)
		{
			handles[j] = _beginthread(count, 0, NULL);
		}
		WaitForMultipleObjects(10, handles, 1, INFINITE);
		printf("%d time g_count = %d\n", i, g_count);
		//重置
		g_count = 0;
	}
	getchar();
	return 0;
}

运行结果:
在这里插入图片描述
理论上,g_count的最后结果应该是100,可事实却并非如此,不但结果不一定是100,而且每次的结果还很可能不一样。
原因是,多个线程对同一个全局变量进行访问时,特别是在进行修改操作,会引起冲突。

设置断点,查看反汇编
在这里插入图片描述
g_count++;被分为三步操作:

  1. 把g_count的内容从内存中移动到寄存器eax
  2. 把寄存器eax加1
  3. 把寄存器eax中的内容移动到内存g_count的地址

三步以后,g_count的值被顺利加1。

分析:

  • 一条最简单加减法语句都有可能会被翻译成几条指令执行;为了避免语句在CPU这一层级上的指令交叉带来的行为不可知,在多线程程序设计时我们必须通过一些方式来进行规范
  • CPU执行的时间片内包含多条指令,每条指令执行期间不会被打断
  • 但如果 一个操作包含多条指令,则 *很有可能该操作会被打断 *。g_count++ 就是这样的操作。

g_count被顺利加到100的前提:每次加1,都建立在上一次加1顺利完成的基础上。也就是说,如果上一次加1被打断,这一次的加1就得不到上一次加1的累积效果。自然,最后的结果,多半会小于100。

2. 原子操作

  • 为了解决线程冲突,引入原子操作。

  • 所谓原子操作,是指 不会被线程调度机制打断的操作,操作一旦开始,就得执行到结束为止。

  • 原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分。

  • 原子操作一般靠 底层汇编 实现。

  • 在头文件 winnt.h 中提供了很多的原子操作函数,它们使用 自加锁 的方式,保证操作的原子性,如 自增操作InterlockedIncrement

解决冲突的最常见做法是:

  • 使用互斥锁
  • 其大概的模型就是篮球模式:几个人一起抢球,谁抢到了谁玩,玩完了再把球丢出来重新抢;
  • 互斥锁是操作系统这一层级 的,最终 映射到CPU上也是一堆指令,是指令就必然会带来额外的开销.

既然CPU指令是多线程不可再分的最小单元,那我们如果有办法将代码语句和指令对应起来,不就不需要引入互斥锁从而提高性能了吗? 而这个对应关系就是所谓的原子操作.

函数原型

LONG CDECL_NON_WVMPURE InterlockedIncrement(	_Inout_ _Interlocked_operand_ LONG volatile *Addend);

其它相关操作,请自行查看头文件。

使用该函数,我们修改线程函数count。
修改很简单,只需把 g_count++ 改为 InterlockedIncrement((LONG)&g_count) 即可

运行如下:
在这里插入图片描述
显然,在原子操作下,我们肯定是可以得到正确结果的。

C++11中的原子操作

#include <atomic>

std::atomic_int myInt;

在C++11的atomic中有两种做法:

  • 模拟, 比如说对于一个atomic类型,我们可以给他附带一个mutex,操作时lock/unlock一下,这种在多线程下进行访问,必然会导致线程阻塞;
  • 有相应的CPU层级的对应,这就是一个标准的lock-free类型. 可以通过is_lock_free函数,判断一个atomic是否是lock-free类型.

自旋锁

使用原子操作模拟互斥锁的行为就是自旋锁
互斥锁状态是由操作系统控制的,自旋锁的状态是程序员自己控制的

  • 线程反复检查锁变量是否可用。
  • 由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值