【发布时间】:2022-01-17 02:43:02
【问题描述】:
使用 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 也没有报告上述程序有任何问题。
我主要对读者线程读取的值有一点疑问。
C++ 标准是否保证,无论底层 CPU 架构如何,
a) 上述程序没有任何 UB?
b) reader 线程只能看到 writer 线程写入的最新值?
********* 其他详细信息 ********
请注意,以上是一个简短的示例程序,我试图在其中复制我的主要项目设计的特定部分。那里的规模要大得多。例如那里的数组大小(不完全是数组,但非常相似)约为 200 万。此外,数据结构不是简单的 int,而是自定义的可序列化结构。
所以想想这样的事情:
custom_serializable_struct shared_variable[2000000];
在我的主程序中,会有 'N' 个写入线程和一个单个读取线程。大多数情况下,编写器线程将正常工作。由于 N 远小于 200 万,因此我在编写器线程中使用单独的同步(200 万个索引中的每一个都有 1 个 std::atomic_flag。这是在获取 shared_timed_mutex 后使用的)(我已经从示例代码的设计,因为我觉得它与我的要求无关)。
就像我上面所说的,大多数时候,编写器线程都可以工作。只有偶尔,阅读器线程才会起作用。
该方案主要有以下要求:
- 当读取线程工作时,我必须尽量减少写入线程在互斥体上花费的等待时间。
- 我必须确保读取器线程在工作时始终获取写入器线程写入的最新值。
所以基本上这就是我的主程序中发生的事情:
N 个编写器线程:
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 个读者话题:
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.
}
【问题讨论】:
-
如果写线程只获得了一个共享锁并写入共享数据,那么你将与任何其他只有一个共享锁并正在读取的线程竞争。 (如果您唯一的另一个线程总是获得排他锁,则没有竞争,但是当一个简单的互斥锁可以做到时,为什么还要首先使用读/写锁,并且不会让代码的人类读者感到困惑?)
-
@NicolBolas 数组的 5 个元素中的每一个都是一个单独的内存位置。没有两个写入器线程会触及相同的内存位置。
-
互斥锁不仅仅是将线程锁定在临界区之外。他们还建立了memory barriers,其中,在某些架构上,可能不止一种。事实上,我不知道这一点,但是当线程在“共享”模式下获取锁时执行的特定内存屏障指令似乎可能会为将要写入的线程提供不充分的同步 共享变量。同样,对于要读取另一个线程所写内容的线程来说,排他锁可能是错误的。
-
@JeremyFriesner
rand() -
@n.1.8e9-where's-my-sharem。感谢您指出了这一点。我已经尝试修复它。
标签: c++ multithreading c++14