安全地 "lend" 内存块到 C 中的另一个线程,假设没有 "concurrent access"

Safely "lend" memory block to another thread in C, assuming no "concurrent access"

问题

我想在一个线程中分配内存,安全地 "lend" 指向另一个线程的指针,以便它可以读取该内存。

我使用的是可翻译成 C 的高级语言。高级语言有线程(未指定线程 API,因为它是跨平台的——见下文)并支持标准的 C 多线程原语,如 atomic-compare-exchange,但它没有真正记录(没有使用例子)。 这种高级语言的约束是:

现在这对于大型(不想复制)或可变大小(我认为数组大小是类型的一部分)消息是不切实际的。我想发送这样的消息,下面是我要如何实现它的概要:

我需要知道如何确保它在没有数据竞争的情况下工作。我的理解是我需要使用内存栅栏,但我不完全确定循环中的哪一个(ATOMIC_RELEASE,...)和位置(或者我是否需要)。


便携性考虑

因为我的高级语言需要跨平台,所以我需要答案来工作:

如果有帮助,我假设以下架构:

并使用以下编译器(您可以假定所有编译器的 "recent" 版本):

到目前为止,我只在 Windows 上构建过,但是当应用程序完成后,我想以最少的工作将它移植到其他平台。因此,我从一开始就尝试确保跨平台兼容性。


尝试的解决方案

这是我假定的工作流程:

  1. 从队列中读取所有消息,直到它为空(只有当它完全为空时才会阻塞)。
  2. 在这里叫一些"memory fence"?
  3. 读取消息内容(消息中指针的目标),并处理消息。
    • 如果消息是 "request",则可以对其进行处理,并将新消息缓冲为 "replies"。
    • 如果消息是一个"reply",原来"request"的消息内容可以freed(隐式请求"ack")。
    • 如果消息是 "reply",并且它本身包含指向 "reply content" 的指针(而不是 "inline reply"),那么也必须发送 "reply-ack" .
  4. 在这里叫一些"memory fence"?
  5. 将所有缓冲的消息发送到适当的消息队列中。

实际代码太大 post。这是使用互斥量(如消息队列)的简化伪代码(足以显示如何访问 shared 内存):

static pointer p = null
static mutex m = ...
static thread_A_buffer = malloc(...)

Thread-A:
  do:
    // Send pointer to data
    int index = findFreeIndex(thread_A_buffer)
    // Assume different value (not 42) every time
    thread_A_buffer[index] = 42
    // Call some "memory fence" here (after writing, before sending)?
    lock(m)
    p = &(thread_A_buffer[index])
    signal()
    unlock(m)
    // wait for processing
    // in reality, would wait for a second signal...
    pointer p_a = null
    do:
      // sleep
      lock(m)
      p_a = p
      unlock(m)
    while (p_a != null)
    // Free data
    thread_A_buffer[index] = 0
    freeIndex(thread_A_buffer, index)
  while true

Thread-B:
  while true:
    // wait for data
    pointer p_b = null
    while (p_b == null)
      lock(m)
      wait()
      p_b = p
      unlock(m)
    // Call some "memory fence" here (after receiving, before reading)?
    // process data
    print *p_b
    // say we are done
    lock(m)
    p = null
    // in reality, would send a second signal...
    unlock(m)

这个解决方案行得通吗?重新表述问题,Thread-B 是否打印“42”? 始终在所有考虑的平台和 OS(pthreads 和 Windows CS)上? 或者我是否需要添加其他线程原语,例如内存栅栏?


研究

我花了几个小时查看许多相关的 SO 问题,并阅读了一些文章,但我仍然不完全确定。根据@Art 的评论,我可能不需要做任何事情。我相信这是基于 POSIX 标准 4.12 内存同步的声明:

[...] using functions that synchronize thread execution and also synchronize memory with respect to other threads. The following functions synchronize memory with respect to other threads.

我的问题是这句话没有明确说明它们是指 "all the accessed memory" 还是 "only the memory accessed between lock and unlock." 我读过有人对这两种情况争论不休,甚至有人暗示它是故意写得不准确的, 给编译器实现者更多的实现空间!

此外,这适用于 pthreads,但我需要知道它如何适用于 Windows 线程。

我会选择任何答案,基于 quotes/links 来自标准文档或其他高度可靠的来源,证明我不需要围栏 显示我需要哪些围栏 ,在上述平台配置下,对于 Windows/Linux/MacOS 情况,至少 。如果 Windows 线程在这种情况下表现得像 pthreads,我也想要一个 link/quote。

以下是我阅读的一些(最好的)相关 questions/links,但存在相互矛盾的信息使我怀疑自己的理解。

我也将 Nim 用于个人项目。 Nim 有一个垃圾收集器,你必须避免它,因为你使用它的 C 调用线程的内存处理例程:

https://nim-lang.org/docs/backends.html

在 Linux 中,malloc 使用内部互斥锁来避免并发访问造成的损坏。我认为 Windows 也是如此。您可以自由使用内存,但需要避免多次'free'或访问冲突(您必须保证只有一个线程在使用内存并且可以'free')。

您提到您使用自定义堆实现。这个堆可能可以从其他线程访问,但是你必须检查这个库是否不会对另一个线程正在处理的指针执行 'free'。如果此自定义堆实现是 Nim 的垃圾收集器,那么您必须不惜一切代价避免它并执行内存访问的自定义 C 实现,并使用 Nim 的 C 调用进行内存 malloc 和 free。

如果你想拥有平台独立性,那么你需要使用 os 和 c:

的多重集中
  1. 使用互斥锁和解锁进行同步。
  2. 使用条件变量向其他线程发送信号。
  3. 在分配给其他线程时使用堆内存保持递增,一旦访问over.this就递减它,将避免无效释放。

我对 C++11 的文档和 C11:n1570.pdf 中类似措辞的回顾使我得出以下理解。

如果在线程之间执行某种形式的合作同步,则数据可以在线程之间安全使用。如果有一个队列,它在互斥锁中从队列中读取一个项目,并且如果在持有互斥锁的同时将项目添加到队列中,那么第二个线程中可读的内存将是已写入的内存第一个线程。

这是因为不允许编译器和底层 CPU 基础架构组织通过顺序传递的副作用。

来自 n1570

An evaluation A inter-thread happens before an evaluation B if A synchronizes with B, A is dependency-ordered before B, or, for some evaluation X:

— A synchronizes with X and X is sequenced before B,

— A is sequenced before X and X inter-thread happens before B, or

— A inter-thread happens before X and X inter-thread happens before B

所以要保证新线程可见的内存是一致的,那么下面就可以保证结果了

  • 访问锁的互斥锁
  • 生产者的互锁写入 + 消费者的互锁读取

互锁写入,导致线程 A 上的所有先前操作在线程 B 看到读取之前被排序并刷新缓存。

将数据写入 "other thread processing" 队列后,第一个线程无法安全地(解锁)修改或读取对象中的任何内存,直到它知道(通过某种机制)另一个线程不再访问数据。如果这是通过某种同步机制完成的,它只会看到正确的结果。

C++ 和 C 标准都旨在规范编译器和 CPU 的现有行为。因此,尽管在使用 pthreads 和 C99 标准方面没有那么正式的保证,但这些保证是一致的。

根据你的例子

线程 A

int index = findFreeIndex(thread_A_buffer)

这一行有问题,因为它没有显示任何同步原语。如果 findFreeIndex 的机制只依赖于线程 A 写入的内存,那么这将起作用。如果线程 B 或任何其他线程修改内存,则需要进一步锁定。

lock(m)
p = &(thread_A_buffer[index])
signal()
unlock(m)

这由....

涵盖

15 An evaluation A is dependency-ordered before an evaluation B if

— A performs a release operation on an atomic object M, and, in another thread, B performs a consume operation on M and reads a value written by any side effect in the release sequence headed by A, or

— for some evaluation X, A is dependency-ordered before X and X carries a dependency to B.

18 An evaluation A happens before an evaluation B if A is sequenced before B or A interthread happens before B.

同步前的操作"happen before"同步,同步后保证在其他线程可见

加锁(获取)和解锁(释放),确保线程A中的信息有一个严格的顺序完成并且对B可见。

thread_A_buffer[index] = 42;      // happens before 

目前内存 thread_A_buffer 在 A 上可见,但在 B 上读取它会导致未定义的行为。

lock(m);  // acquire

虽然发布需要,但我看不到获取的任何结果。

p = &thread_A_buffer[index];
unlock(m);

A 的所有指令流现在对 B 可见(由于它与 m 同步)。

thread_A_buffer[index] = 42;  << This happens before and ...
p = &thread_A_buffer[index];  << carries a dependency into p
unlock(m);

A 中的所有内容现在都对 B 可见,因为

An evaluation A inter-thread happens before an evaluation B if A synchronizes with B, A is dependency-ordered before B, or, for some evaluation X

— A synchronizes with X and X is sequenced before B,

— A is sequenced before X and X inter-thread happens before B, or

— A inter-thread happens before X and X inter-thread happens before B.

pointer p_a = null
do:
  // sleep
  lock(m)
  p_a = p
  unlock(m)
while (p_a != null)

这段代码是完全安全的,读入p_a的值会和其他线程一起排序,在线程b中同步写入后不会为空。同样,lock/unlock 导致严格排序,确保读取值将是写入值。

线程 B 的所有交互都在一个锁中,因此再次完全安全。

如果 A 在将对象提供给 B 之后修改该对象,那么它将无法工作,除非有一些进一步的同步。