ndk*_*pel 5 c++ sockets multithreading network-programming memory-model
可以依赖外部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.
}
Run Code Online (Sandbox Code Playgroud)
我们假设远程机器只发送数据,s2在接收到"foo"从s1.如果这个假设失败,那么肯定会产生未定义的行为.但如果它成立(并且没有其他外部故障发生,如网络数据损坏等),该程序是否会产生定义的行为?
"从不","未指定(取决于实现)","取决于send/recv实现提供的保证"是我期望的那种示例答案,最好是C++标准(或其他相关标准)的理由,例如POSIX for socket/networking).
如果"从不",则更a改为std::atomic<int>初始化为确定值(比如0)将避免未定义的行为,但是保证读取的值是42 thread2或者是否可以读取过时的值?POSIX套接字是否提供了进一步的保证,确保不会读取过时的值?
如果"依赖",POSIX套接字是否提供相关保证以使其定义行为?(如果是s1和s2相同的套接字而不是两个单独的套接字怎么样?)
作为参考,标准I/O库有一个子句,当使用iostreams时,它似乎提供了类似的保证(N4604中的27.2.32):
如果一个线程进行库调用,它会将值写入流,结果,另一个线程通过库调用b从流中读取此值,这样就不会导致数据竞争,然后写入与之同步b读了.
那么是否使用底层网络库/函数提供类似的保证?
实际上,似乎编译器不能重新排序a关于send和recv函数的全局访问(因为它们a原则上可以使用).但是,除非/ pair本身提供某种内存屏障/同步保证thread2,a否则线程运行仍然可以读取过时的值.sendrecv
简短的回答:不,不存在a会更新的通用保证。我的建议是发送awith的值"foo"- 例如"foo, 42"或类似的值。这保证可以工作,而且可能不会有那么大的开销。[当然可能还有其他原因导致效果不佳]
长篇大论,并不能真正解决问题:
不保证全局数据在多核处理器的不同内核中立即“可见”,无需进一步操作。是的,大多数现代处理器都是“一致的”,但并非所有品牌的所有型号都保证这样做。因此,如果 thread2 运行在已经缓存了 的副本的处理器上a,则不能保证a调用时 的值是 42 f。
C++标准保证全局变量在函数调用后加载,因此编译器不允许这样做:
tmp = a;
recv(...);
f(tmp);
Run Code Online (Sandbox Code Playgroud)
但正如我上面所说,可能需要缓存操作来保证所有处理器同时看到相同的值。如果send和recv的时间足够长或访问量足够大[没有直接的衡量标准来说明有多长或多大],您可能会在大多数甚至所有时间看到正确的值,但不能保证普通类型实际上是正确的在最后写入该值的线程之外更新。
std::atomic对某些类型的处理器有帮助,但不能保证它在更改后的任何合理时间在第二个线程或第二个处理器核心上“可见”。
唯一实用的解决方案是使用某种“重复直到我看到它发生变化”类型代码 - 这可能需要一个值(例如)一个计数器,以及一个值作为实际值 - 如果你希望能够说“a现在是42。我又设置了a,这次也是42”。如果a表示,例如缓冲区中可用的数据项的数量,那么重要的可能是“它改变了值”,并且只需检查“这与上次相同”。这些std::atomic操作在排序方面有保证,这允许您使用它们来确保“如果我更新此字段,则保证其他字段同时或在此之前出现”。因此,您可以使用它来保证例如一对数据项设置为“有一个新值”(例如指示当前数据的“版本号”的计数器)和“新值是 X” 。
当然,如果您知道您的代码将在什么处理器架构上运行,您就可以对行为将是什么做出更高级的猜测。例如,所有 x86 和许多 ARM 处理器都使用缓存接口来实现变量的原子更新,因此通过在一个内核上执行原子更新,您可以知道“没有其他处理器会拥有该变量的过时值”。但有些可用的处理器没有此实现细节,并且即使使用原子指令,更新也不会在其他内核或其他线程中更新,直到“未来某个时间,不确定何时”。