可以依赖外部 I/O 作为跨线程同步的一种形式吗?
具体来说,请考虑下面的伪代码,它假定存在网络/套接字函数:
int a; // Globally accessible data.
socket s1, s2; // Platform-specific.
int main() {
// Set up + connect two sockets to (the same) remote machine.
s1 = ...;
s2 = ...;
std::thread t1{thread1}, t2{thread2};
t1.join();
t2.join();
}
void thread1() {
a = 42;
send(s1, "foo");
}
void thread2() {
recv(s2); // Blocking receive (error handling omitted).
f(a); // Use a, should be 42.
}
我们假设远程机器只发送数据到s2
收到"foo"
来自 s1
.如果这个假设失败,那么肯定会导致未定义的行为。但如果它成立(并且没有其他外部故障发生,如网络数据损坏等),这个程序是否会产生定义的行为?
“从不”、“未指定(取决于实现)”、“取决于发送/接收的实现所提供的保证”是我所期待的那种示例答案,最好是从 C++ 标准(或其他相关标准,例如用于套接字/网络的 POSIX)。
如果“从不”,则更改 a
成为std::atomic<int>
初始化为一个确定的值(比如 0)会避免未定义的行为,但是在 thread2
中保证该值被读取为 42|还是可以读取过时的值? POSIX 套接字是否提供进一步的保证以确保不会读取过时值?
如果“依赖”,POSIX 套接字是否提供相关保证以使其定义行为? (如果 s1
和 s2
是同一个 socket 而不是两个独立的 socket 怎么样?)
作为引用,标准 I/O 库有一个子句似乎在使用 iostream 时提供类似的保证(N4604 中的 27.2.3¶2):
If one thread makes a library call a that writes a value to a stream and, as a result, another thread reads this value from the stream through a library call b such that this does not result in a data race, then a’s write synchronizes with b’s read.
那么是否使用底层网络库/函数来提供类似的保证?
实际上,编译器似乎无法重新排序对全局 a
的访问关于 send
和 recv
功能(因为他们原则上可以使用 a
)。但是,线程正在运行 thread2
仍然可以读取 a
的陈旧值除非send
提供了某种内存屏障/同步保证。/recv
对自己。
最佳答案
简短回答:不,不存在a
将被更新的通用保证。我的建议是将 a
的值与 "foo"
一起发送 - 例如“foo, 42”
,或类似的东西。这保证有效,而且可能不会有那么大的开销。 [当然可能还有其他原因导致效果不佳]
没有真正回答问题的长篇大论:
不保证全局数据在多核处理器的不同内核中立即“可见”而无需进一步操作。是的,大多数现代处理器都是“连贯的”,但并非所有品牌的所有型号都保证如此。因此,如果 thread2 运行在一个已经缓存了 a
拷贝的处理器上,则无法保证在您调用 时
.a
的值是 42 >f
C++标准保证全局变量在函数调用后加载,所以编译器不允许这样做:
tmp = a;
recv(...);
f(tmp);
但正如我上面所说,可能需要缓存操作来保证所有处理器同时看到相同的值。如果 send
和 recv
的时间足够长或访问次数足够大 [没有直接的衡量方法可以说明多长时间或多长时间] 你可能会看到大部分甚至所有的正确值的时间,但对于普通类型,不能保证它们实际上是在最后写入值的线程之外更新的。
std::atomic
将在某些类型的处理器上有所帮助,但不能保证它在第二个线程或第二个处理器核心上在任何合理的时间“可见”改变了。
唯一实用的解决方案是使用某种“重复直到我看到它发生变化”类型的代码——这可能需要一个值(例如)一个计数器,一个值是实际值——如果你想要的话能够说“a 现在是 42。我又设置了 a,这次也是 42”。如果 a
表示,例如缓冲区中可用数据项的数量,可能是“它更改了值”很重要,只需检查“这是否与上次相同”。 std::atomic
操作在排序方面有保证,这允许您使用它们来确保“如果我更新这个字段,另一个字段保证同时或在此之前出现”。所以你可以用它来保证例如一对数据项被设置为“有一个新值”(例如一个计数器来指示当前数据的“版本号”)和“新值是 X” .
当然,如果您知道您的代码将在哪些处理器架构上运行,您就可以合理地对行为是什么做出更高级的猜测。例如,所有 x86 和许多 ARM 处理器都使用缓存接口(interface)来实现变量的原子更新,因此通过在一个内核上进行原子更新,您可以知道“没有其他处理器会有这个过时的值”。但是有些可用的处理器没有此实现细节,并且更新,即使是原子指令,也不会在其他内核或其他线程中更新,直到“ future 某个时间,不确定何时”。
关于c++ - C++中依赖网络I/O提供跨线程同步,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49034629/