使用 std::shared_timed_mutex 的通常模式是讓“reader”執行緒以共享模式獲取它,“writer”執行緒以獨占模式獲取它。通過這種方式,讀取和寫入不能同時發生,因此程式不會出現資料競爭/未定義行為。
我想了解如果我更改執行緒之間的模式是否有任何問題,即讀取器執行緒在以獨占模式獲取鎖后讀取共享變數,而寫入器執行緒在共享模式下獲取互斥鎖后寫入共享變數。
#include <iostream>
#include <thread>
#include <random>
#include <chrono>
#include <shared_mutex>
using namespace std::chrono_literals;
std::shared_timed_mutex lck;
int shared_array[5];
void writerFunc(int index);
void readerFunc();
//main thread
int main() {
std::thread writer_threads[5];
for(int i=0; i<5; i) {
writer_threads[i] = std::thread(writerFunc,i);
}
while(true) {
std::this_thread::sleep_for(5s);
readerFunc();
}
for(int i=0; i<5; i) {
writer_threads[i].join();
}
}
//function executed in writer threads.
//Each writer thread will work on it's own index in the global shared array.
void writerFunc(int index) {
std::random_device rd;
std::mt19937 mt(rd());
std::uniform_real_distribution<double> dist(1.0, 42.0);
while(true) {
{
std::shared_lock<std::shared_timed_mutex> sl(lck);
//Writing random number in shared variable.
shared_array[index] = dist(mt);
}
std::this_thread::sleep_for(100ms);
}
}
//function executed in reader thread(main).
void readerFunc() {
std::lock_guard<std::shared_timed_mutex> sl(lck);
for(int i=0; i<5 ; i) {
std::cout<<"\nshared_array["<<i<<"]--> "<<shared_array[i];
}
std::cout<<"\n\n";
}
由于讀寫執行緒不能同時并發訪問變數,因此上述程式中不存在資料競爭。Thread-sanitiser 也沒有報告上述程式有任何問題。
我主要是對閱讀器執行緒讀取的值有一點疑問。
無論底層 CPU 架構如何,是否由 C 標準保證,
a) 上述程式沒有任何 UB?
b) 讀取執行緒只能看到寫入執行緒寫入的最新值?
*******其他詳細資訊********
請注意,上面是一個簡短的示例程式,我試圖在其中復制主專案設計的特定部分。那邊的規模要大很多。例如,陣列的大小(不完全是陣列,但非常相似)大約有 200 萬。此外,資料結構不是簡單的 int 而是自定義的可序列化結構。
所以想想這樣的事情:
custom_serializable_struct shared_variable[2000000];
在我的主程式中,將有“N”個撰寫器執行緒和一個讀取器執行緒。大多數時候,作家執行緒將作業。由于 N 遠小于 200 萬,因此我在寫入執行緒之間使用單獨的同步(200 萬個索引中的每一個都有 1 個 std::atomic_flag。這是在獲取 shared_timed_mutex 之后使用的)(我從示例代碼的設計,因為我覺得它與我的要求無關)。
就像我上面所說的那樣,大多數情況下,作家執行緒將起作用。只有偶爾,讀者執行緒會起作用。
該方案主要有以下要求:
- 我必須盡量減少在讀取器執行緒作業時在互斥鎖上花費的寫入器執行緒的等待時間。
- I've to ensure that the reader thread, whenever it works, always gets the latest value written by the writer threads.
So basically this is what is happening in my main program:
N writer threads:
while (true) {
// 1. Acquire the shared_timed_mutex in shared mode.
// 2. Acquire the std::atomic_flag of the index, i, on which the thread has to work. This is required, as I mentioned, to prevent data race among writer threads.
// 3. Do changes in the custom_serializable_struct shared_variable[i]
}
1 reader thread:
while(true) {
// 1. long sleep time.
// 2. Acquire the shared_timed_mutex in exclusive mode.
// 3. read the entire 2 million values. Please note that this read is not done 1 by 1 like in a for loop. It's more like memcpy of the entire memory.
}
uj5u.com熱心網友回復:
但為什么?
我想知道改變讀者和作者在鎖定方面的角色背后的動機是什么!你這樣做解決了什么問題?
在之前的評論中,您提到您不希望作者之間發生爭用。
查看代碼,我還推斷int陣列中每個的更新都獨立于其他人,但讀者必須同時看到它們,就好像它們共同具有一個含義(排他鎖的原因)。你還沒有提到這一點 - 所以假設這不是你的意圖。
只有一個讀者,但有很多作者,即它看起來倒轉為讀者多于作者的(一些?)陳規定型案例。這不應該是主要考慮因素。
應避免傳達意外的含義和令人驚訝的代碼。我同意@Nicol Bolas 并建議另一種方法:
錯誤的工具 -std::atomic改用
std::shared_timed_mutex對于未來的維護者(你自己?)來說,這里的倒置使用是一個驚喜。另外,使用它是對讀者產生誤導資訊的來源,也是這個問題的原因。我同意@Nicol Bolas 的觀點,atomic 可以解決這個問題:
std::atomic<int> shared_array[5];
void writerFunc(int index) {
///Other code
while(true) {
//Writing random number in shared variable.
shared_array[index].fetch_add(dist(mt));
std::this_thread::sleep_for(100ms);
}
}
void readerFunc() {
for (auto& item : shared_array) {
std::cout << item;
}
}
更好的抽象 - 使用 libguarded::shared_guarded
悲哀的根源似乎是您應用的級別std::shared_timed_mutex lck- 它控制整個陣列,而您希望對每個元素進行更精細的控制。
我強烈建議您考慮使用在 BSD 2-Clause“Simplified”許可下可用shared_guarded的cs_libguarded。
libguarded::shared_guarded<int> shared_array[5]; //Nailed it!
void writeFunc(int index) {
//Other code
while (true) {
{
auto handle = shared_array[index].lock();
auto& item = *handle;
item = dist(mt);
}
std::this_thread::sleep_for(100ms);
}
}
void readerFunc() {
for (auto& array_element : shared_array) {
auto item = array_element.lock_shared();
std::cout << *item;
}
}
以上不僅確保了共享資料的正確使用,而且還確保了const 的正確性,因為它不允許寫入lock_shared. 這可以與任何資料型別一起使用,而不僅僅是ints - 一個限制std::atomic。正如@Solomon Slow 指出的那樣,記憶體障礙可能會導致使用原始方法亂序執行的意外結果 - 這段代碼沒有這個問題。libguarded還確保對共享資料的訪問始終與正確同步- 不會意外使用共享資料。
僅供參考,shared_guarded相當于為每個元素使用互斥鎖(如下所示),只會更干凈、正確且萬無一失。
std::shared_timed_mutex lck[5]; //Don't do it by hand, better use libguarded, as above
int shared_array[5];
我強烈建議優先考慮更清潔的實作,而不是像不想有很多mutexes這樣的任意目標。如果您不想要爭用,請消除共享而不是旨在減少互斥鎖。問題在于共享而不是互斥鎖的存在。
PS:您將問題標記為 C 14,而 libguarded 需要 C 17。據我檢查,libguarded::shared_guarded應該與std::shared_timed_mutex.
uj5u.com熱心網友回復:
unlock_shared 顯式地與lock同一互斥鎖上的后續呼叫同步。這將允許讀取器讀取由任何寫入器寫入的資料。同樣,lock_shared與之前對 的呼叫同步unlock。因此可以在shared_mutex沒有資料競爭的情況下使用向后(注意:rand不需要是執行緒安全的)。
但是……應該嗎?
互斥鎖的目的是確保資料完整性,不僅僅是在位元組級別(即:資料競爭),而是在更高級別。您有 5 個執行緒寫入 5 個不同的位置。但是……資料是什么意思?這些資料是否彼此完全不同,或者資料集合是否具有某種需要保留的意義?也就是說,如果一個執行緒寫入一個值,如果另一個執行緒還沒有寫入它的值,讀取器是否會得到格式錯誤的資訊?
如果這些資料值都是完全獨立的,那么互斥鎖是不必要的(至少對于基本型別)。你真正在做的只是原子寫入。作者可以寫一個atomic<T>,而讀者將閱讀這些。由于這些值都是完全不同的并且它們之間沒有任何排序問題,因此您不需要阻止任何執行緒進行寫入。您需要做的就是確保個人層面的資料完整性T。無鎖原子將比任何基于互斥鎖的解決方案快得多。
但是,如果資料具有某種完整性的概念,如果執行緒組共同創建讀取器執行緒應該完整讀取的單個值,那么您要查找的是barrier,而不是互斥鎖。該物件允許您查看一組執行代理是否已共同到達某個點。如果他們有,您可以安全地讀取資料。一旦你讀完它,就可以安全地釋放代理再次寫信給他們。
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/382674.html
標籤:c multithreading c 14
下一篇:使用執行緒時如何減少時間偏差?
