C++11 多线程:数据保护


在编写多线程程序时,多个线程同时访问某个共享资源,会导致同步的问题,这篇文章中我们将介绍 C++11 多线程编程中的数据保护。

数据丢失

让我们从一个简单的例子开始,请看如下代码:

 
01 #include <iostream>
02 #include <string>
03 #include <thread>
04 #include <vector>
05   
06 using std::thread;
07 using std::vector;
08 using std::cout;
09 using std::endl;
10   
11 class Incrementer
12 {
13     private:
14         int counter;
15   
16     public:
17         Incrementer() : counter{0} { };
18   
19         void operator()()
20         {
21             for(int i = 0; i < 100000; i++)
22             {
23                 this->counter++;
24             }
25         }
26   
27         int getCounter() const
28         {
29             return this->counter;
30         }       
31 };
32   
33 int main()
34 {
35     // Create the threads which will each do some counting
36     vector<thread> threads;
37   
38     Incrementer counter;
39   
40     threads.push_back(thread(std::ref(counter)));
41     threads.push_back(thread(std::ref(counter)));
42     threads.push_back(thread(std::ref(counter)));
43   
44     for(auto &t : threads)
45     {
46         t.join();
47     }
48   
49     cout << counter.getCounter() << endl;
50   
51     return 0;
52 }

这个程序的目的就是数数,数到30万,某些傻叉程序员想要优化数数的过程,因此创建了三个线程,使用一个共享变量 counter,每个线程负责给这个变量增加10万计数。

这段代码创建了一个名为 Incrementer 的类,该类包含一个私有变量 counter,其构造器非常简单,只是将 counter 设置为 0.

紧接着是一个操作符重载,这意味着这个类的每个实例都是被当作一个简单函数来调用的。一般我们调用类的某个方法时会这样 object.fooMethod(),但现在你实际上是直接调用了对象,如 object(). 因为我们是在操作符重载函数中将整个对象传递给了线程类。最后是一个 getCounter 方法,返回 counter 变量的值。

再下来是程序的入口函数 main(),我们创建了三个线程,不过只创建了一个 Incrementer 类的实例,然后将这个实例传递给三个线程,注意这里使用了 std::ref ,这相当于是传递了实例的引用对象,而不是对象的拷贝。

现在让我们来看看程序执行的结果,如果这位傻叉程序员还够聪明的话,他会使用 GCC 4.7 或者更新版本,或者是 Clang 3.1 来进行编译,编译方法:

 
1 g++ -std=c++11 -lpthread -o threading_example main.cpp

运行结果:

 
01 [lucas@lucas-desktop src]$ ./threading_example 
02 218141
03 [lucas@lucas-desktop src]$ ./threading_example 
04 208079
05 [lucas@lucas-desktop src]$ ./threading_example 
06 100000
07 [lucas@lucas-desktop src]$ ./threading_example 
08 202426
09 [lucas@lucas-desktop src]$ ./threading_example 
10 172209

但等等,不对啊,程序并没有数数到30万,有一次居然只数到10万,为什么会这样呢?好吧,加1操作对应实际的处理器指令其实包括:

 
1 movl    counter(%rip), %eax
2 addl    $1, %eax
3 movl    %eax, counter(%rip)

首个指令将装载 counter 的值到 %eax 寄存器,紧接着寄存器的值增1,然后将寄存器的值移给内存中 counter 所在的地址。

我听到你在嘀咕:这不错,可为什么会导致数数错误的问题呢?嗯,还记得我们以前说过线程会共享处理器,因为只有单核。因此在某些点上,一个线程会依照指令执行完成,但在很多情况下,操作系统会对线程说:时间结束了,到后面排队再来,然后另外一个线程开始执行,当下一个线程开始执行时,它会从被暂停的那个位置开始执行。所以你猜会发生什么事,当前线程正准备执行寄存器加1操作时,系统把处理器交给另外一个线程?

我真的不知道会发生什么事,可能我们在准备加1时,另外一个线程进来了,重新将 counter 值加载到寄存器等多种情况的产生。谁也不知道到底发生了什么。

  • 1
  • 2
  • 下一页

相关内容