歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

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 值加載到寄存器等多種情況的產生。誰也不知道到底發生了什麼。

Copyright © Linux教程網 All Rights Reserved