这段代码需要同步吗?
Does this code need synchronization?
我计划在我的游戏项目中编写多线程部分:
线程 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++,如果有帮助的话。
竞争条件通常仅在两个线程尝试非原子地同时读取-修改-写入相同数据时出现。在这种情况下,只有一个线程写入(线程A),而另一个线程读取(线程B)。
唯一的 "incorrectness" 如您所说,如果对象已加载但计数器尚未递增。这导致 B 读取 stale 数据,因为 load-and-increment 操作不是自动执行的。
如果你不介意这个无辜的异常,那么它工作得很好。 :)
如果这让您烦恼,那么您需要一次执行所有 加载和递增 语句(通过使用锁或任何其他同步原语)。
您必须考虑内存可见性/缓存。如果没有内存屏障,这很可能导致几秒钟的延迟,直到数据对线程 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:如果您确定由多个线程访问的数据,请使用同步。多线程错误很难跟踪和重现,所以最好一起避免它们。
我计划在我的游戏项目中编写多线程部分:
线程 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++,如果有帮助的话。
竞争条件通常仅在两个线程尝试非原子地同时读取-修改-写入相同数据时出现。在这种情况下,只有一个线程写入(线程A),而另一个线程读取(线程B)。
唯一的 "incorrectness" 如您所说,如果对象已加载但计数器尚未递增。这导致 B 读取 stale 数据,因为 load-and-increment 操作不是自动执行的。
如果你不介意这个无辜的异常,那么它工作得很好。 :)
如果这让您烦恼,那么您需要一次执行所有 加载和递增 语句(通过使用锁或任何其他同步原语)。
您必须考虑内存可见性/缓存。如果没有内存屏障,这很可能导致几秒钟的延迟,直到数据对线程 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:如果您确定由多个线程访问的数据,请使用同步。多线程错误很难跟踪和重现,所以最好一起避免它们。