我正在开发个人爱好时间游戏引擎,而我正在研究多线程批量执行程序。我最初在整个地方使用并发无锁队列和std :: function来促进主线程和从线程之间的通信,但决定废弃它以支持更轻量级的做事方式,让我能够严密控制内存分配:函数指针和内存池。
无论如何,我遇到了一个问题:
无论我尝试什么,函数指针只能由一个线程正确读取,而其他线程读取空指针,从而使断言失败。
我很确定这是缓存问题。我已经确认所有线程都有相同的指针地址。我已经尝试将它声明为volatile,intptr_t,std :: atomic,并尝试了各种cast-fu和线程,似乎只是忽略它并继续读取它们的缓存副本。
我在模型检查器中对主服务器和从服务器进行了建模,以确保并发性良好,并且没有活锁或死锁(假设共享变量都正确同步)
void Executor::operator() (int me) {
while (true) {
printf("Slave %d waiting.\n", me);
{
std::unique_lock<std::mutex> lock(batch.ready_m);
while(!batch.running) batch.ready.wait(lock);
running_threads++;
}
printf("Slave %d running.\n", me);
BatchFunc func = batch.func;
assert(func != nullptr);
int index;
if (batch.store_values) {
while ((index = batch.item.fetch_add(1)) < batch.n_items) {
void* data = reinterpret_cast<void*>(batch.data_buffer + index * batch.item_size);
func(batch.share_data, data);
}
}
else {
while ((index = batch.item.fetch_add(1)) < batch.n_items) {
void** data = reinterpret_cast<void**>(batch.data_buffer + index * batch.item_size);
func(batch.share_data, *data);
}
}
// at least one thread finished, so make sure we won't loop back around
batch.running = false;
if (running_threads.fetch_sub(1) == 1) { // I am the last one
batch.done = true; // therefore all threads are done
batch.complete.notify_all();
}
}
}
void Executor::run_batch() {
assert(!batch.running);
if (batch.func == nullptr || batch.n_items == 0) return;
batch.item.store(0);
batch.running = true;
batch.done = false;
batch.ready.notify_all();
printf("Master waiting.\n");
{
std::unique_lock<std::mutex> lock(batch.complete_m);
while (!batch.done) batch.complete.wait(lock);
}
printf("Master ready.\n");
batch.func = nullptr;
batch.n_items = 0;
}
batch.func
由另一个函数
template<typename SharedT, typename ItemT>
void set_batch_job(void(*func)(const SharedT*, ItemT*), const SharedT& share_data, bool byValue = true) {
static_assert(sizeof(SharedT) <= SHARED_DATA_MAXSIZE, "Shared data too large");
static_assert(std::is_pod<SharedT>::value, "Shared data type must be POD");
assert(std::is_pod<ItemT>::value || !byValue);
assert(!batch.running);
batch.func = reinterpret_cast<volatile BatchFunc>(func);
memcpy(batch.share_data, (void*) &share_data, sizeof(SharedT));
batch.store_values = byValue;
if (byValue) {
batch.item_size = sizeof(ItemT);
}
else { // store pointers instead of values
batch.item_size = sizeof(ItemT*);
}
batch.n_items = 0;
}
这里是它正在处理
的struct(和typedef)typedef void(*BatchFunc)(const void*, void*);
struct JobBatch {
volatile BatchFunc func;
void* const share_data = operator new(SHARED_DATA_MAXSIZE);
intptr_t const data_buffer = reinterpret_cast<intptr_t>(operator new (EXEC_DATA_BUFFER_SIZE));
volatile size_t item_size;
std::atomic<int> item; // Index into the data array
volatile int n_items = 0;
std::condition_variable complete; // slave -> master signal
std::condition_variable ready; // master -> slave signal
std::mutex complete_m;
std::mutex ready_m;
bool store_values = false;
volatile bool running = false; // there is work to do in the batch
volatile bool done = false; // there is no work left to do
JobBatch();
} batch;
如何确保对batch.func
的所有必要读取和写入在线程之间正确同步?
以防万一:我正在使用Visual Studio并编译x64 Debug Windows可执行文件。 Intel i5,Windows 10,8GB RAM。
答案 0 :(得分:0)
所以我对C ++内存模型进行了一些阅读,并设法使用atomic_thread_fence破解了一个解决方案。一切都可能超级破碎,因为我疯了,不应该在这里推出我自己的系统,但是,嘿,学习很有趣!
基本上,每当你写完你想要其他线程看的东西时,你需要拨打atomic_thread_fence(std::memory_order_release)
在接收线程上,您在阅读共享数据之前调用atomic_thread_fence(std::memory_order_acquire)
。
在我的情况下,应该在等待条件变量之前立即进行释放,并且应该在使用其他线程写入的数据之前立即进行获取。
这确保了其他人可以看到一个线程上的写入。
我不是专家,所以这可能不是解决问题的正确方法,可能会面临某些厄运。例如,我仍然有一个死锁/活锁问题需要解决。
tl; dr:它不是一个缓存的东西:除非你用原子内存栅栏强制执行,否则线程可能没有完全相互同步的数据。