談C++執行緒安全容器的設計
阿新 • • 發佈:2018-12-30
最近看到一本書,《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 一個執行緒安全的佇列實現樣例. 其中
- void push(T new_value)
- {
- std::shared_ptr<T> data(
- std::make_shared<T>(std::move(new_value)));
- std::lock_guard<std::mutex> lk(mut);
- data_queue.push(data);
- data_cond.notify_one();
- }
其次,函式內部使用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的實現。
- Value value_for(Key const& key,Value const& default_value) const
- {
- boost::shared_lock<boost::shared_mutex> lock(mutex);
- bucket_iterator const found_entry=find_entry_for(key);
- return (found_entry==data.end())?
- default_value : found_entry->second;
- }
這裡是設計為一個執行緒安全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示範程式碼,也有不小心中招的時候,所以,碼農們還是小心耕耘,仔細編碼,反覆推敲吧。
- // Please note that MinGW32 compilers currently do not support <thread>. Use MinGW64 builds like TDM-GCC instead.
- #include <thread>
- using std::thread;
- #include <vector>
- using std::vector;
- #include <stdio.h>
- struct ThreadItem {
- char* result; // could've used stringstream too, but don't like their syntax
- thread worker;
- };
- void* ThreadFunction(char** result) {
- *result = new char[256]; // new 分配了記憶體, 卻沒有釋放
- snprintf(*result,256,"Hello World from thread ID %d",
- std::this_thread::get_id());
- }
- int main() {
- // Get the amount of "processing units"
- int n = std::thread::hardware_concurrency();
- // Create array of threads
- vector<ThreadItem> threadlist;
- threadlist.resize(n);
- // Spawn a thread for each core
- for(int i = 0;i < n;i++) {
- threadlist[i].worker = thread(ThreadFunction,&threadlist[i].result); // pass rand() as data argument
- }
- // Wait for them all to finish
- for(int i = 0;i < n;i++) {
- threadlist[i].worker.join();
- }
- // Present their calculation results
- printf("Results:\n");
- for(int i = 0;i < n;i++) {
- printf("%s\n",threadlist[i].result);
- }
- }