在編寫多線程程序時,多個線程同時訪問某個共享資源,會導致同步的問題,這篇文章中我們將介紹 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 值加載到寄存器等多種情況的產生。誰也不知道到底發生了什麼。