我计划在游戏项目中编写多线程部分:
线程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 ++,如果有帮助的话。
答案 0 :(得分:2)
您必须考虑内存可见性/缓存。如果没有内存屏障,这很可能会导致数秒延迟,直到线程B (1)可以看到数据。
这适用于这两种数据:Counter
和Objects
列表。
C ++ 11标准(2)保证只有在您不引入竞争条件时才能正确执行多线程程序。没有同步,您的程序基本上具有未定义的行为(3)。但是,在实践中它可能没有。
是的,使用互斥锁并同步对Counter
和Objects
的访问权限。
(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 操作未以原子方式执行。
如果你不介意这种无辜的异常,那么它就可以了。 :)
如果这让您烦恼,那么您需要一次执行所有加载和增量语句(通过使用锁或任何其他同步原语)。