Re: [問題] 關於std::mutex的應用

作者: sarafciel (Cattuz)   2020-04-30 01:55:13
※ 引述《icetofux ()》之銘言:
: 標題: [問題] 關於std::mutex的應用
: 時間: Mon Apr 27 22:26:40 2020
:
: 開發平台(Platform): (Ex: Win10, Linux, ...)
: Win10/Linux
: 編譯器(Ex: GCC, clang, VC++...)+目標環境(跟開發平台不同的話需列出)
: GCC
: 額外使用到的函數庫(Library Used): (Ex: OpenGL, ...)
: None
: 問題(Question):
:
: 最近在使用C++11的std::thread,我已經知道若要在不同的thread存取同一個變數,
: 必須使用mutex來做管理才能達到Thread-Safe。
:
: 目前遇到的問題是,若我有多個不同的變數分別必須在多個不同的thread內存取,我
: 除了變數名稱外,還必須一一產生對應的mutex,我想建立下列這樣的樣板類別:
:
: template <class T>
: class SharedVariable {
: private:
: std::mutex mtx;
: T data;
: public:
: T Get(void) {
: T data_cpy;
: std::lock_guard<std::mutex> lck(mtx);
: data_cpy = this->data;
: return data_cpy;
: }
: void Set(const T data) {
: std::lock_guard<std::mutex> lck(mtx);
: this->data = data;
: }
: };
:
: 在產生變數物件的同時,該物件也同時具有一個不用額外命名的mutex,並且當我
: 透過Get/Set存取變數時,也自動做好了上鎖、解鎖的功能。
:
: SharedVariable<int> shared_int;
: SharedVariable<std::string> shared_string;
: SharedVariable<std::vector<double>> shared_vector;
:
: shared_int.Set(123);
: int a = shared_int.Get();
:
: 目前比較讓我有疑慮的是,在不同的thread內使用物件本身(如上例的shared_int、
: shared_string、shared_vector)是一個Thread-Safe的行為嗎?我不確定要如何判
: 斷,想請有經驗的先進指教。
想回的東西比較多,就單獨回一篇。
有錯還請務必指正。
thread-safe比較簡單的一種判斷方式是,當某個thread在中途被context switch掉
其他的thread看到的狀態到底是不是對的,如果是不對的,
你不能給他看,白話文講就是要鎖起來
不考慮OOE跟memory barrier,某個物件跟兩條thread的關係可以粗略分三種:
1.兩條都只有讀
2.一條讀另一條讀/寫
3.兩條都要做讀/寫
第1種case就例如我先建一個質數表給兩個thread查,
因為這個質數表的狀態是不會變的,這個時候是不用鎖的
第2種case的例子就有點像你開DMA buffer給某個硬體裝置寫,
你再去讀這個buffer做處理
這時候寫的那邊不用顧慮讀錯的問題,因為只有他寫
他看到的狀態一定是最新,只讀的那邊則要去確保他看到的是最新的狀態,
視處理的任務而定,也有可能要做到每次狀態變更都要能知道
所以通常是讀的那端會做polling,或是寫的打interrupt通知之類的手段
第3種才是最常見的,兩邊都要做讀寫,此時就會有race condition的問題
但這不表示說,你把讀寫都各加一道大鎖就會沒事
因為讀寫有時候會有相依性,這個相依性會導致你要像原推文steve大講的那樣
你要保證他的順序性,或是在寫之前,你要確保他讀到的data是最新狀態才會對
後面這個就是compare and swap在做的事情
最簡單的例子就是i++,因為你寫回去i的值跟i本來的值有關
也就是我那段code寫的那樣,你的Get跟Set中間因為有一個解鎖的空隙
在這個空隙裡,如果thread A有context switch,B 就有可能拿到lock
導致讀寫序變成: A讀->B讀->A寫->B寫
或是A讀->B讀->B寫->A寫
而不論是哪個順序,最後都會變成A或B的其中一條thread對i的影響被蓋掉
這是為什麼i++這種操作會弄成atomic的fetch_add的原因
因為你就是要把他的順序涵蓋進critical section裡他才會對
另外就是你這邊Get出去都是copy,這樣子雖然可以保證thread有各自的instance
但在Set的時候就會變成最終只有幾條thread的modify是有效的
而你就算是改用reference,因為物件的operation沒有鎖
這裡不加鎖就沒辦法保證正確性了
所以一般是不會像你這樣用在存取上加鎖的方式來處理多執行緒問題,
因為真的要做,你可能至少要把物件提供的operation都包一層鎖,
而這樣還不夠,因為還是會有上面提到的讀寫相依性的問題要解決
那如果要做到這個地步,還不如看情況在function內做針對性的加鎖來的省事XD
而與其去直面多thread同時讀寫多個物件這個難題
不如像love大跟kobe大講的那樣
把物件的寫權交給單個thread,然後其他thread透過concurrent queue
去下command給這個thread寫,
這樣子相當於是把第3種case給抽象成第2種case來做,會好處理很多
:
: 謝謝。
:
: 餵入的資料(Input):
: None
: 預期的正確結果(Expected Output):
: None
: 錯誤結果(Wrong Output):
: None
: 程式碼(Code):(請善用置底文網頁, 記得排版,禁止使用圖檔)
: None
: 補充說明(Supplement):
:
:
:
作者: icetofux   2020-04-30 07:16:00
謝謝你的範例跟說明,因為我的情況屬於第二種案例,所以我的做法確實沒考慮到第三種案例的可能會發生的缺陷。

Links booklink

Contact Us: admin [ a t ] ucptt.com