问题描述
我的代码可以处理具有不同布局的大数据块。布局将决定哪些数据是固定的,哪些数据是不固定的。一旦数据被固定在一个块中,它通常不会再改变。所以所有代码读取数据总会看到相同的数据。
然而,其他服务可能会在这些块中进行更改,只要它们确定没有代码会读取块的那部分。为了简化代码,无论块的布局如何,包含更改的块都将从一个服务发送到另一个服务。然后接收服务将覆盖整个块,包括未更改的数据。让我用一个例子来说明这一点:
假设我们有以下数据块:
57 | 23 | 98 | 17 | 25 | 00 | 00 | 00 | 00 | 00 |
---|
假设前 5 个值是“固定的”。我们服务中的代码只会读取前 5 个值,而永远不会读取接下来的 5 个值。由于我们的架构设计,我们可以保证这一点。接下来的 5 个值没有意义,所以我在表格中加了零来说明这一点。
现在另一个服务确定接下来的 5 个值,将完整块发送到我们的服务,我们只需用新数据覆盖完整块。由于前 5 个值是“固定的”,它们保持不变,但是传输和覆盖块的代码不知道块的布局,因此它唯一能做的就是覆盖整个块。结果如下:
57 | 23 | 98 | 17 | 25 | 08 | 33 | 42 | 71 | 85 |
---|
如前所述,前 5 个值没有改变,尽管它们被传输逻辑覆盖了。
问题是:这是一场数据竞赛吗?如果其他线程可以同时读取数据,是否允许覆盖具有完全相同值的内存地址?
解决方法
这是一场数据竞赛吗?
是的。
如果其他线程可以同时读取数据,是否允许覆盖具有完全相同值的内存地址?
没有明确说明 - 这也不是唯一的问题。
如果您的编译器实际上执行了单个 8 字节的加载,那么您将在最后 3 个字节上进行真实(即,甚至可能不仅仅是理论上的)数据竞争。假设您有一台假设机器,其中 uint64_t
值 57 23 98 17 25 00 00 42
是陷阱表示,您的更新线程使用 memmove
,并且它向后复制更新。 >
然而,数据竞争意味着行为是未定义的标准。它可能在特定平台上有明确定义 - 例如没有整数陷阱表示的任何平台、您知道编译器将真正使用字节加载的任何平台,或任何具有显式语义的幂等存储平台。
参见,例如,[intro.races] note 23:
引入对潜在共享内存位置的推测读取的转换可能不会保留本文档中定义的 C++ 程序的语义,因为它们可能会引入数据竞争。 但是,它们通常在优化编译器的上下文中有效,该编译器针对具有明确定义的数据竞争语义的特定机器。 对于不容忍竞争或提供硬件竞争检测的假设机器,它们将无效
(对于非投机种族没有这样的说明,对于幂等存储也没有特定的例外,但在 IMO 上采取相同的方法是合理的)。
显然,如果您可以编写代码,使其不依赖于这些平台细节,那么在面对编译器和/或平台更新时,它会更加可移植且不那么脆弱。只是在块的两个版本之间进行原子交换(因此您从保证不变的副本中读取,并保证不会共享的副本写入)将始终正确,并且由于减少缓存/一致性流量甚至可能更快。
,有太多的“视情况而定”,无法用明确的陈述来回答这个问题。
如果这个结构是一些高效代码的一部分,那么答案是:肯定是的。可能会出现竞争条件。必须使用任务同步机制。没有讨论的可能。