原子操作

十、原子操作

本节主要记录原子操作std::atomic类模板的使用。

1、原子操作概念引出

有两个线程,对一个全局变量进行操作,一个线程读变量值,另一个线程向这个变量中写值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include "pch.h"
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

using namespace std;

int global_val = 0; // 创建一个全局变量
//std::mutex global_mutex; // 全局互斥锁

void mythread()
{
for (int i = 0; i < 10000000; ++i)
{
//global_mutex.lock();
global_val++;
//global_mutex.unlock();
}

return;
}
int main()
{
auto t1 = std::chrono::steady_clock::now();
std::thread thread1(mythread);
std::thread thread2(mythread);

thread1.join();
thread2.join();

auto t2 = std::chrono::steady_clock::now();
double dr_s = std::chrono::duration<double>(t2 - t1).count();

cout << "全局变量的值 " << global_val << endl;
cout << "耗时 " << dr_s<<" 秒" << endl;
return 0;
}

上述代码创建了两个线程,两个线程都是对全局变量global_val进行++运算。常理上讲,当两个线程执行完毕并返回后,global_val = 20000000。但是运行上述代码,我们发现并不是这样,global_val<<20000000 !这是因为我们进行++操作时,尽管看起来只有一行代码,但是实际上编译之后会产生多行汇编代码,在进行++运算的过程中,系统切换到了另外一个线程,打断了原来线程的++操作。

知道了问题所在,我们便有对应的解决方法,使用我们之前学到的互斥量,对共享数据进行保护,使得其他线程不会打断本线程的操作,上述代码会进行20000000次的加锁、解锁操作,尽管比较费时,但是结果是正确的,程序耗时大约5秒。

原子操作,一般都是指”不可分割的操作“,也就是说这种操作状态要么是完成的,要么是未完成的,不可能出现半完成状态。原子操作适用于上述的情况,我们可以把原子操作理解成一种:不需要用到互斥量加锁技术的多线程并发编程方式。或者也可以理解成:原子操作是在多线程中不会被打断的程序执行片段。

互斥量的加锁一般是针对一个代码段(几行代码),而原子操作针对的一般都是一个变量,原子操作效率上比互斥量更胜一筹。

C++11中的std::atomic来代表原子操作,它是一个类模板,用来封装某个类型的值。

2、std::atomic的基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<atomic>
std::atomic<int> global_val = 0; // 创建一个全局变量

void mythread()
{
for (int i = 0; i < 10000000; ++i)
{
global_val++;
global_val = global + 1; // 不支持
}

return;
}

std::atomic对int进行包装之后,我们可以向原来使用int一样使用包装之后的变量。使用原子操作之后,同样的操作只需要1.5秒左右。

一般std::atomic原子操作,针对++、–、+=、&=、|=、*=运算符是支持的,其他的可能不支持。

听说打赏我的人,最后都找到了真爱。