并行编程有那么难吗?-计数(Counting)

计数可以说是比较常见也是并发的书本中最开始讲述的例子,本章将会讲述简单的计数需要面临的问题。

简单的计数操作

非原子计数操作

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
#include <iostream>
#include <pthread.h>
int64_t counter = 0;
int nTimes = 100000;

void *inc_count(void *){
for(int i=0; i<nTimes; i++){
counter++;
}
}

int64_t read_count(void){
return counter;
}


int main(int argc, char* argv[]){
int nThreads = 2;
pthread_t *tid =static_cast<pthread_t*> (malloc(nThreads*sizeof(pthread_t)));
/* create number of nThreads threads*/
for(int j= 0; j < nThreads; j++){
pthread_create(&tid[j], NULL, inc_count, NULL);
}

/* wait for the threads to finish worrk*/
for(int j= 0; j < nThreads; j++){
pthread_join(tid[j], NULL);
}

int res = read_count();
std::cout << "the final counter value = " << res << std::endl;

}

以上是一个多线程并发程序,对一个变量进行++操作,可以看出,通过运行结果可以看出,其最后的输出结果不等于nThreads*nTimes。这就可以看出在非原子操作的情况下。这种并发是无法满足精确地++操作的。

原子操作的计数

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 <pthread.h>
#include <atomic>
#include "linux/atomic.h"
using namespace std;

atomic_t counter = new ATOMIC_INIT(0);
int nTimes = 100000;

void *inc_count(void *){
for(int i=0; i<nTimes; i++){
atomic_inc(&counter);
}
}

int64_t read_count(void){
return atomic_read(&counter);
}


int main(int argc, char* argv[]){
int nThreads = 2;
pthread_t *tid =static_cast<pthread_t*> (malloc(nThreads*sizeof(pthread_t)));
for(int j= 0; j < nThreads; j++){
pthread_create(&tid[j], NULL, inc_count, NULL);
}

for(int j= 0; j < nThreads; j++){
pthread_join(tid[j], NULL);
}

int res = read_count();
std::cout << "the final counter value = " << res << std::endl;

}

以上是使用原子操作的代码。但是很遗憾。一直没有运行起来,报错就说

error: ‘atomic_t’ does not name a type
延迟随着并发数量变化图

可以看出来。理想状况下,延迟增加图应该是靠近x轴的曲线,但是实际情况是当CPU核数增加,其延迟也在增加,并没有起到并发的作用。这就给并发带来了挑战!
延迟随着并发数量变化图

可以看一个对全局变量进行原子操作的例子,如图2所示,In order for each CPU to get a chance to increment a given global variable, the cache line containing that variable must circulate among all the CPUs, as shown by the red arrows. Such circulation will take significant time, resulting in the poor performance seen in Figure 5.1(图1)。