多线程C ++:强制从内存中读取,绕过缓存

时间:2017-02-18 06:13:39

标签: c++ multithreading caching

我正在开发个人爱好时间游戏引擎,而我正在研究多线程批量执行程序。我最初在整个地方使用并发无锁队列和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。

1 个答案:

答案 0 :(得分:0)

所以我对C ++内存模型进行了一些阅读,并设法使用atomic_thread_fence破解了一个解决方案。一切都可能超级破碎,因为我疯了,不应该在这里推出我自己的系统,但是,嘿,学习很有趣!

基本上,每当你写完你想要其他线程看的东西时,你需要拨打atomic_thread_fence(std::memory_order_release)

在接收线程上,您在阅读共享数据之前调用atomic_thread_fence(std::memory_order_acquire)

在我的情况下,应该在等待条件变量之前立即进行释放,并且应该在使用其他线程写入的数据之前立即进行获取。

这确保了其他人可以看到一个线程上的写入。

我不是专家,所以这可能不是解决问题的正确方法,可能会面临某些厄运。例如,我仍然有一个死锁/活锁问题需要解决。

tl; dr:它不是一个缓存的东西:除非你用原子内存栅栏强制执行,否则线程可能没有完全相互同步的数据。