此代码是否需要同步?

时间:2017-01-04 14:54:41

标签: multithreading thread-safety race-condition

我计划在游戏项目中编写多线程部分:

线程A:从磁盘加载一堆对象,这需要几秒钟。加载的每个对象都会增加一个计数器。

线程B:一个游戏循环,我在其中显示加载对象数量的加载屏幕,或者在加载完成后开始操作对象。

在代码中我相信它看起来如下:

Counter = 0;
Objects;

THREAD A:
    for (i = 0; i < ObjectsToLoad; ++i) {
        Objects.push(LoadObject());
        ++Counter;
    }
    return;

THREAD B:
    ...
    while (true) {
        ...
        C = Counter;
        if (C < ObjectsToLoad)
            RenderLoadscreen(C);
        else
            WorkWithObjects(Objects)
        ...
    }
    ...

从技术上讲,这可以算作竞争条件 - 对象可能已加载但计数器尚未递增,因此 B 读取旧值。我还需要在 B 中缓存计数器,这样它的值在检查和渲染之间不会发生变化。

现在的问题是 - 我应该在这里实现任何同步机制,比如使用反原子或引入一些互斥或条件变量?这里的要点是我可以安全地牺牲循环的迭代直到计数器改变。从我得到的,只要 A 只写入值而 B 只检查它,一切都很好。

我和朋友一直在讨论这个问题,但是我们无法达成一致意见,所以我们决定征求对多线程更有能力的人的意见。语言是C ++,如果有帮助的话。

2 个答案:

答案 0 :(得分:2)

您必须考虑内存可见性/缓存。如果没有内存屏障,这很可能会导致数秒延迟,直到线程B (1)可以看到数据。

这适用于这两种数据:CounterObjects列表。

C ++ 11标准(2)保证只有在您不引入竞争条件时才能正确执行多线程程序。没有同步,您的程序基本上具有未定义的行为(3)。但是,在实践中它可能没有。

是的,使用互斥锁并同步对CounterObjects的访问权限。

(1)这是因为每个CPU核心都有自己的寄存器和缓存。如果您不告诉CPU Core A某些其他Core B可能对数据感兴趣,则可以通过以下方式对其进行优化:将数据保留在寄存器中。 Core A必须将数据写入更高级别的内存区域(L2 / L3缓存或RAM),以便Core B可以加载更改。

(2) C ++ 11之前的任何版本都不关心多线程。通过第三方库支持互斥,原子等,但语言本身与线程无关 请参阅:C++11 introduced a standardized memory model. What does it mean? And how is it going to affect C++ programming?

(3)问题是你的代码可以在不同的阶段重新排序(为了更有效的执行):在编译器,汇编器和CPU上。您必须通过原子或互斥锁添加内存屏障,告诉计算机哪些指令需要按顺序保留。这在大多数语言中都是一样的。

我建议观看有关C ++ 11内存模型的这些非常有趣的视频:
atomic<> weapons by Herb Sutter

IMO:如果您识别多个线程访问的数据,请使用同步。多线程错误很难跟踪和重现,所以最好一起避免它们。

答案 1 :(得分:0)

竞争条件通常仅在两个线程尝试非原子读取 - 修改 - 同时写入相同数据时。在这种情况下,只有一个线程写入(线程A),而另一个线程读取(线程B)。

唯一的&#34;不正确&#34;正如你所说,你遇到的是,如果对象已经加载但是计数器没有增加。这导致B读取过时的数据,因为 load-and-increment 操作未以原子方式执行。

如果你不介意这种无辜的异常,那么它就可以了。 :)

如果这让您烦恼,那么您需要一次执行所有加载和增量语句(通过使用锁或任何其他同步原语)。