假设没有"并发访问",安全地将内存块"借出"给C中的另一个线程

Seb*_*iot 21 c multithreading thread-safety nim-lang

问题

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

我正在使用一种翻译成C语言的高级语言.高级语言有线程(未指定的线程API,因为它是跨平台的 - 见下文)并支持标准的C多线程原语,如原子比较交换,但它没有真正记录(没有用法示例).这种高级语言的限制是:

  • 每个线程执行一个事件处理无限循环.
  • 每个线程都有自己的本地堆,由一些自定义分配器管理.
  • 每个线程都有一个"输入"消息队列,可以包含来自任意数量的不同其他线程的消息.
  • 传递队列的消息是:
    1. 对于固定类型的消息
    2. 使用复制

现在,对于大型(不希望复制)或可变大小(我认为数组大小是类型的一部分)消息,这是不切实际的.我想发送这样的消息,这里是我想要实现它的大纲:

  • 消息(请求回复)可以存储"有效负载"内联(复制,固定限制总值大小),或指向发送方堆中数据的指针
  • 消息内容(发送者堆中的数据)由发送线程拥有(分配和释放)
  • 接收线程在完成消息内容时向发送线程发送确认
  • "发送"线程在发送之后不得修改消息内容,直到收到(ack).
  • 应该永远是对内存并行读访问被写入,写之前完成.这应该由消息队列work-flow保证.

我需要知道如何确保这没有数据竞争.我的理解是我需要使用内存栅栏,但我不完全确定哪一个(ATOMIC_RELEASE,...)以及循环中的位置(或者我是否需要任何内容​​).


便携性考虑因素

因为我的高级语言需要跨平台,所以我需要得到答案:

  • Linux,MacOS以及可选的Android和iOS
    • 使用pthreads原语来锁定消息队列:pthread_mutex_initpthread_mutex_lock+pthread_mutex_unlock
  • 视窗
    • 使用Critical Section Objects来锁定消息队列:InitializeCriticalSectionEnterCriticalSection+LeaveCriticalSection

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

  • 用于Windows/Linux/MacOS的英特尔/ AMD PC架构(?).
  • 适用于iOS和Android的未知(ARM?)

并使用以下编译器(您可以假设所有这些编译器的"最近"版本):

  • Windows上的MSVC
  • Linux上的铿锵声
  • Xcode在MacOS/iOS上
  • 适用于Android的CodeWorks for Android

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


试图解决方案

这是我假设的工作流程:

  1. 读取队列中的所有消息,直到它为空(仅当它完全为空时才阻塞).
  2. 在这里叫一些"记忆围栏"?
  3. 读取消息内容(消息中指针的目标),并处理消息.
    • 如果消息是"请求",则可以处理该消息,并将新消息缓冲为"回复".
    • 如果消息是"回复",则可以释放原始"请求"的消息内容(隐式请求"确认").
    • 如果消息是"回复",并且它本身包含指向"回复内容"的指针(而不是"内联回复"),那么也必须发送"回复确认".
  4. 在这里叫一些"记忆围栏"?
  5. 将所有缓冲的消息发送到适当的消息队列中.

真正的代码太大而无法发布.这里简化了(足以显示如何访问共享内存)使用互斥锁的伪代码(如消息队列):

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)
Run Code Online (Sandbox Code Playgroud)

这个解决方案有用吗?重新提出问题,Thread-B打印"42"?总是,在所有考虑过的平台和操作系统(pthreads和Windows CS)上?或者我是否需要添加其他线程原语,如内存栅栏?


研究

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

[...]使用同步线程执行的函数,并使内存与其他线程同步.以下函数使内存与其他线程同步.

我的问题是这句话没有明确说明它们是指"所有被访问的内存",还是"只有在锁定和解锁之间访问的内存".我读过人们为这两种情况辩护的问题,甚至有些人暗示它是故意写的,是为了让编译器实现者在实现方面有更多的余地!

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

我会根据标准文档中的引用/链接或其他一些高度可靠的来源选择任何答案,证明我不需要围栏或在上述平台配置下显示我需要的围栏,至少适用于Windows/Linux/MacOS案例.如果在这种情况下Windows线程的行为类似于pthreads,我也想要一个链接/引用.

以下是我读过的一些(最好的)相关问题/链接,但是存在冲突信息会让我怀疑我的理解.

mks*_*eve 2

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

\n\n

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

\n\n

这是因为编译器和底层 CPU 基础设施不允许组织通过排序的副作用。

\n\n

从 n1570

\n\n
\n

如果 A 与 B 同步,则 A 线程间评估发生在 B 评估之前,A\n 在 B 之前按依赖顺序排序,或者,对于某些评估 X:

\n\n

\xe2\x80\x94 A 与 X 同步,并且 X 在 B 之前排序,

\n\n

\xe2\x80\x94 A 在 X 之前排序,并且 X 线程间在 B 之前发生,或者

\n\n

\xe2\x80\x94 A 线程间发生在 X 之前,X 线程间发生在 B 之前

\n
\n\n

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

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

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

\n\n

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

\n\n

C++ 和 C 标准都旨在形式化编译器和 CPU 的现有行为。因此,尽管在 pthread 和 C99 标准的使用方面存在不太正式的保证,但预计这些保证是一致的。

\n\n

从你的例子来看

\n\n

螺纹A

\n\n
int index = findFreeIndex(thread_A_buffer)\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
lock(m)\np = &(thread_A_buffer[index])\nsignal()\nunlock(m)\n
Run Code Online (Sandbox Code Playgroud)\n\n

这涵盖了....

\n\n
\n

15 评估 A 在评估 B 之前按依赖性排序,如果

\n\n

\xe2\x80\x94 A 对原子对象 M 执行释放操作,而在另一个线程中,B 对 M 执行消费操作并读取以 A 为首的释放序列中任何副作用写入的值,或者

\n\n

\xe2\x80\x94 对于某些评估 X,A 在 X 之前是依存顺序的,并且 X 对 B 具有\n 依存关系。

\n
\n\n

\n\n
\n

18 如果 A 在 B 之前排序,或者 A 线程间\n 在 B 之前发生,则评估 A 在评估 B 之前发生。

\n
\n\n

同步之前的操作“发生在”同步之前,并且保证在同步之后在其他线程中可见。

\n\n

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

\n\n
thread_A_buffer[index] = 42;      // happens before \n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
lock(m);  // acquire\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
p = &thread_A_buffer[index];\nunlock(m);\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
thread_A_buffer[index] = 42;  << This happens before and ...\np = &thread_A_buffer[index];  << carries a dependency into p\nunlock(m);\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
\n

如果 A 与 B 同步,A 的依赖顺序先于 B,或者对于某些评估 X,则评估 A 线程间在评估 B 之前发生

\n\n

\xe2\x80\x94 A 与 X 同步,并且 X 在 B 之前排序,

\n\n

\xe2\x80\x94 A 在 X 之前排序,并且 X 线程间在 B 之前发生,或者

\n\n

\xe2\x80\x94 A 线程间发生在 X 之前,X 线程间发生在 B 之前。

\n
\n\n
pointer p_a = null\ndo:\n  // sleep\n  lock(m)\n  p_a = p\n  unlock(m)\nwhile (p_a != null)\n
Run Code Online (Sandbox Code Playgroud)\n\n

这段代码是完全安全的,读入 p_a 的值将与另一个线程排序,并且在线程 b 中同步写入后将不为空。同样,锁定/解锁会导致严格的排序,以确保读取的值将是写入的值。

\n\n

线程 B 的所有交互都在锁内,因此也是完全安全的。

\n\n

如果 A 在将对象提供给 B 后修改该对象,那么它将不起作用,除非有进一步的同步。

\n