与 memcpy 的数据竞争,未定义的行为?

Data race with memcpy, undefined behavior?

我在一个线程中写入一个内存区域(memcpy),然后在另一个线程中将其复制到一个新位置memcpy。有时这些操作可能会重叠,从而导致数据竞争。具有数据竞争的程序调用未定义的行为并且无效。

在这种情况下,我在复制后检查复制的数据是否有效(实际上没有发生竞争。)如果确实发生了竞争,我将丢弃复制的数据。但是,据我所知,这并没有让我摆脱关于 UB 的困境。我认为是否使用数据竞争的结果仍然是UB。

现在我可以在汇编中编写自己的 memcpy 例程(或者只是复制并粘贴 libc 中的例程),这将避开整个 UB 问题。汇编不是 C++,汇编中发生的任何事情都不会授予编译器调用鼻恶魔的许可[1]。顺便说一句,对于内联汇编以及外部编译和链接的汇编来说都是如此吗?尽管 memcpy 已经在任何现代 libc 中汇编,但编译器也可以对其进行特殊处理,编译器通常会针对已知大小和对齐方式进行优化,例如小型内联 memcpy - 这可能会再次调用鼻恶魔.

我是不是想多了?很难想象一个编译器如此神乎其神以至于它可以在编译时检测到数据竞争——同时又如此愚蠢以至于优化器使用它来生成错误代码而不是报告它。但是编译器最近有办法突破这两个限制 - 所以我觉得有必要在 Stack Overflow 上寻求建议。

[编辑] 由于很多人对我如何在此处同步事物感到好奇,所以让我解释一下。指向正在复制的内存的指针在线程之间共享。它是通过 atomic load(mo_acquire) 访问的。然后将内存复制到新位置。然后是 LoadLoad barrier,接着是指针的第二个 load(mo_relaxed)。如果指针不匹配,复制的结果将被丢弃,因为另一个线程可能在复制期间与该线程竞争。写入内存的线程首先使用 atomic store(mo_relaxed) 更新指向 null 的指针,然后是 StoreStore barrier 和 racing memcpy。因此,虽然在不同线程中对 memcpy 的两次调用可能是数据竞争 - 实际上总是会检测到这种情况,并且在这种情况下总是会丢弃结果。我将此方案称为读时复制,并使用它允许在对象被逐出之后但在内存被重新使用之前在缓存中恢复对象,而不涉及任何互斥锁或 "strong" 同步。

[1]:我期待一个更文明的时代,编译器报告 UB 而不是滥用它进行可能与程序员期望的行为相反的优化。

你基本上是对的。仅仅因为一个执行线程看到复制的数据是 "valid",并不意味着另一个执行线程会看到同样的东西。

为了让其他执行线程看到某些操作的效果,无论是 memcpy() 还是其他任何操作,其他执行线程必须 "sequenced" 执行相关操作。

这只是一个粗略的、不准确的总结。在测序上泼了很多墨。这不是一个简单的主题,有很多选项和规则。

但概括地说,实现线程安全和线程一致行为的最简单方法是使用互斥锁来保护您用来在线程之间传递数据块的共享内存区域。只要每个线程在访问共享内存区之前,读或写获得一个互斥锁,那么所有的线程都会一个快乐家人。

同步锁使用的方法与您正在做的非常相似,尽管只占用非常小的内存。如果数据竞争发生率高,同步锁会更快,但如果竞争率低,您的方法实际上可能更快。

虽然memcpy的结果是undefined,但这不是undefined behavior,只要能检测到是否发生了race,知道是否忽略垃圾结果即可。

这听起来不像您 运行 存在违反保护或类似崩溃错误的风险;我对 memcpy 的使用还不够多,无法知道在重叠操作期间是否存在任何可能崩溃的情况,但我认为它不应该。

因此,只要可以检测到行为,这就不一定是坏事,只要它以明显优于标准方法的方式满足您的需求。我不推荐使用这个 "just because",但是如果你需要传统锁无法获得的速度,并且你可以用你通常提供的任何方式非常彻底地记录定义明确但非标准的行为维护文档,可以接受。

至于编译器优化评论,我从未见过编译器依赖未定义的行为来优化代码,并且由于 C++ 编译器需要根据 C++ 规范保证特定行为,我会立即停止使用任何编译器为此目的依赖于未定义的行为。库代码专门记​​录了不支持且不应执行跨线程同时 read/write 操作,因此以这种方式跨线程使用库代码不属于未定义行为,而是故意滥用库代码您自己承担风险,所有明示或暗示的保证均无效。