cpp11多线程5-atomic

对简单临界资源的访问,如果使用mutex开销较大。

  如有两个线程,对一个变量进行操作,一个线程读这个变量的值,一个线程往这个变量中写值。即使是一个简单变量的读取和写入操作,如果不加锁,也有可能会导致读写值混乱(一条语句可能会被拆成3、4条汇编语句来执行,所以仍然有可能混乱)

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
#include <iostream>
#include <thread>
#include <chrono>

using namespace std;
using namespace std::chrono;
int g_count = 0;

void mythread()
{
for (int i = 0; i < 1000000; i++)
{
g_count++;
}
}

int main()
{
std::thread t1(mythread);
std::thread t2(mythread);
auto start = steady_clock::now();
t1.join();
t2.join();
auto end = steady_clock::now();
auto tt = duration_cast<milliseconds>(end - start);
cout << "程序用时=" << tt.count() << "毫秒" << endl;
cout << "正常情况下结果应该是200 0000次,实际是" << g_count << endl;
return 0;
}

/*
结果:
程序用时=16毫秒
正常情况下结果应该是200 0000次,实际是1303733
*/

使用std::mutex来解决上述对临界资源访问的问题。结果正常,但是每一次循环都要加锁解锁是的程序开销很大。

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
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
using namespace std;
using namespace std::chrono;
int g_count = 0;
std::mutex mymutex;

void mythread()
{
for (int i = 0; i < 1000000; i++)
{
std::unique_lock<std::mutex> u1(mymutex);
g_count++;
}
}

int main()
{
std::thread t1(mythread);
std::thread t2(mythread);
auto start = steady_clock::now();
t1.join();
t2.join();
auto end = steady_clock::now();
auto tt = duration_cast<milliseconds>(end - start);
cout << "程序用时=" << tt.count() << "毫秒" << endl;
cout << "正常情况下结果应该是200 0000次,实际是" << g_count << endl;
return 0;
}

/*
结果:
程序用时=349毫秒
正常情况下结果应该是200 0000次,实际是2000000
*/

std::atomic

  std::atomic包含在头文件中。可以把原子操作理解成一种:不需要用到互斥量加锁(无锁)技术的多线程并发编程方式。原子操作:在多线程中不会被打断的程序执行片段。从效率上来说,原子操作要比互斥量的方式效率要高。互斥量的加锁一般是针对一个代码段,而原子操作针对的一般都是一个变量。原子操作,一般都是指“不可分割的操作”;也就是说这种操作状态要么是完成的,要么是没完成的,不可能出现半完成状态。std::atomic来代表原子操作,是个类模板。其实std::atomic是用来封装某个类型的值。

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
#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>

using namespace std;
using namespace std::chrono;
std::atomic<int> g_count = 0; //封装了一个类型为int的 对象(值)

void mythread()
{
for (int i = 0; i < 1000000; i++)
{
g_count++;
}
}

int main()
{
std::thread t1(mythread);
std::thread t2(mythread);
auto start = steady_clock::now();

t1.join();
t2.join();
auto end = steady_clock::now();
auto tt = duration_cast<milliseconds>(end - start);
cout << "程序用时=" << tt.count() << "毫秒" << endl;
cout << "正常情况下结果应该是200 0000次,实际是" << g_count << endl;
return 0;
}

/*
程序用时=51毫秒
正常情况下结果应该是200 0000次,实际是2000000
*/

一般atomic原子操作,针对++,–,+=,-=,&=,|=,^=是支持的,其他操作不一定支持。如下使用g_count = g_count + 1就会产生错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
std::atomic<int> g_count = 0; //封装了一个类型为int的 对象(值)

void mythread1() {
for (int i = 0; i < 1000000; i++) {
//虽然g_count使用了原子操作模板,但是这种写法既读又写,
//会导致计数错误
g_count = g_count + 1;
}
}

int main() {
std::thread t1(mythread1);
std::thread t2(mythread1);
t1.join();
t2.join();
cout << "正常情况下结果应该是200 0000次,实际是" << g_count << endl;
}

其他需要注意的地方

1
2
std::atomic<int> atm = 0;  
cout << atm << endl;

这里只有读取atm是原子操作,但是整个这一行代码 cout << atm << endl; 并不是原子操作,导致最终显示在屏幕上的值是一个“曾经值”。

1
2
std::atomic<int> atm = 0;
auto atm2 = atm; //不可以

这种拷贝初始化不可以,会报错。

1
atomic<int> atm2(atm.load());

load():以原子方式读atomic对象的值。

1
atm2.store(12);

store():以原子方式写。

参考:

https://www.cnblogs.com/chen-cs/p/13254219.html