1. 程式人生 > >談C++執行緒安全容器的設計

談C++執行緒安全容器的設計

    最近看到一本書,《C++併發程式設計實戰》,[美] Anthony Williams 著,裡面有談及執行緒安全容器的設計及實現程式碼,本人覺得這樣的設計有點問題,問題還是比較明顯的,寫在這裡,供讀者自己思考吧。 

    關於程式碼,可以在這裡下載: https://www.manning.com/books/c-plus-plus-concurrency-in-action

                        或者從本壇部落格中檢視:http://blog.csdn.net/liuxuejiang158blog/article/details/17301739

    這裡只挑選關鍵幾個我認為設計不妥當的地方;

   listing_6.3.cpp  一個執行緒安全的佇列實現樣例. 其中

  1.      void push(T new_value)
  2.      {
  3.         std::shared_ptr<T> data(
  4.             std::make_shared<T>(std::move(new_value)));
  5.         std::lock_guard<std::mutex> lk(mut);
  6.         data_queue.push(data);
  7.         data_cond.notify_one();
  8.      }
    評述:首先,非常明顯,不應該使用物件作形參,至少應該是一個引用吧,減少一次由實參到形參的拷貝構造,以提高效能;
                其次,函式內部使用make_shared, 這又使得型別T又構造了一次, 並呼叫了T的move構造;                <Effective C++> 裡,條款20:寧以pass-by-reference-to-const 替換 pass-by-value 講的也是這個。
     改進辦法:
              執行緒安全的容器,我個人認為不應該存入T的例項,而應該存放指向一個例項的shared_ptr, 或者甚至是unique_ptr;
              其實,書裡的設計也是存放shared_ptr,不明白作者為何介面卻是Vale。  

    listing_6.11.cpp --一個執行緒安全Map的實現。

  1.       Value value_for(Key const& key,Value const& default_value) const
  2.         {
  3.             boost::shared_lock<boost::shared_mutex> lock(mutex);
  4.             bucket_iterator const found_entry=find_entry_for(key);
  5.             return (found_entry==data.end())?
  6.                 default_value : found_entry->second;
  7.         }
      評述:
           這裡是設計為一個執行緒安全map容器;這個函式是根據Key查詢存在容器中的物件,找到了則返回,找不到則返回一個預設值。            我認為介面設計有瑕疵,很容易用錯。
           執行緒安全的容器,不只是容器本身具備執行緒安全,而更主要的是其管理物件(Object of Value )的執行緒安全,更確切地說也不是物件的執行緒安全,而是 物件所具的資料(因為資料可以被複制),它代表的意義在多執行緒併發語義環境下的安全。 對於佇列類,對其管理物件(T)或許要求可以降低,而對於像map, list類,更要求T本身具備執行緒安全要求。 因為queue每次訪問T,都是pop出來,沒有線上程間共享,而map, list是提供了線上程間共享訪問功能的(如上面這個介面),也就是物件(及其資料)在多個執行緒內是共享的,這就要求該被管理物件必須具備執行緒安全性。
           這個介面的錯誤就在於,把內部物件是複製一份出去,外部使用的是一份拷貝的資料。如果執行緒A拷貝了一份,並對資料進行更改, 執行緒B線上程A沒有 更新進容器前又取了一份資料進行操作呢?            <Effective C++> 裡,條款18:讓介面容易被正確使用,不易被誤用。
     改進辦法:
           容器存入的應該是一個指向被管理物件shared_ptr, 查詢返回的也應該是share_ptr, 並要求被管理物件自身一定要滿足執行緒安全特性。 各執行緒通過shared_ptr指向共一個實體物件,使用其提供的執行緒安全的成員函式進行操作。
           如物件不具備執行緒安全,則可考慮unique_ptr,確保應用程序中只有一份資料,這在使用上會有一些不方便,傳入傳出都只能使用右值move操作,而且如果插入容器失敗,還要求插入的介面設計為再將unique_ptr傳回呼叫者, 否則物件會被釋構destory。雖然不便,不過安全。
    Bug 往往出在大意,其實並不是作者水平問題,其實,該書設計的執行緒安全容器裡存放還是share_ptr,出現以上不足,只能怪其信心太足,細心不夠。  再如DEV-C++, 這個IDE提供的自動生成的示例程式碼(Version 5.10): (new-->project-->console-->std::thread), 這裡面有明顯的內容洩漏,程式碼如下。寫在最後,C++的陷阱還是挺多的,即使是出《C++併發實戰》的大牛,又或者是IDE示範程式碼,也有不小心中招的時候,所以,碼農們還是小心耕耘,仔細編碼,反覆推敲吧。 
  1. // Please note that MinGW32 compilers currently do not support <thread>. Use MinGW64 builds like TDM-GCC instead.
  2. #include <thread>
  3. using std::thread;
  4. #include <vector>
  5. using std::vector;
  6. #include <stdio.h>
  7. struct ThreadItem {
  8. char* result; // could've used stringstream too, but don't like their syntax
  9. thread worker;
  10. };
  11. void* ThreadFunction(char** result) {
  12. *result = new char[256];   // new 分配了記憶體, 卻沒有釋放
  13. snprintf(*result,256,"Hello World from thread ID %d",
  14. std::this_thread::get_id());
  15. }
  16. int main() {
  17. // Get the amount of "processing units"
  18. int n = std::thread::hardware_concurrency();
  19. // Create array of threads
  20. vector<ThreadItem> threadlist;
  21. threadlist.resize(n);
  22. // Spawn a thread for each core
  23. for(int i = 0;i < n;i++) {
  24. threadlist[i].worker = thread(ThreadFunction,&threadlist[i].result); // pass rand() as data argument
  25. }
  26. // Wait for them all to finish
  27. for(int i = 0;i < n;i++) {
  28. threadlist[i].worker.join();
  29. }
  30. // Present their calculation results
  31. printf("Results:\n");
  32. for(int i = 0;i < n;i++) {
  33. printf("%s\n",threadlist[i].result);
  34. }
  35. }