2

在今天的一次采访中,被要求将阻塞调用封装到非阻塞。所以我们(面试官和我)决定通过在非阻塞 API 中添加一个后台线程来实现这一点。这是我写的代码:

 30 #define ARRAY_SIZE(x)   (sizeof(x)/sizeof(x[0]))
 31
 32 struct SensorReading records[60*10] = {{0}};
 33 size_t head = 0;
 34 
 35
 36 void * worker_thread(void *arg) {
 37     while (1) {
 38         size_t idx = (head + 1) % ARRAY_SIZE(records);
 39         records[idx] = read_next_sample();
 40         head = idx;
 41     }
 42 }
 43
 44 float get_most_recent_lux() {
 45     static pthread_t worker = -1;
 46     if (-1 == worker) {
 47         struct SensorReading r = read_next_sample(); // This is the blocking call
 48         records[0] = r;
 49         if (-1 == pthread_create(&worker, NULL, worker_thread, NULL)) {
 50             // error handling
 51         }
 52         return r.lux;
 53     }
 54     return records[head].lux;
 55 }

让我在这里稍微解释一下:

  • read_next_sample()是否提供了阻塞调用;
  • 第 44 行get_most_recent_lux()是我需要提供的封装非阻塞 API。
  • 在内部,它启动一个线程来执行第worker_thread()36 行中定义的函数。
  • worker_thread()不断调用阻塞调用并将数据写入 ringbuf。
  • 因此读者可以从 ringbuf 中读取最新的记录数据。

另请注意:

  • 这里使用的这种编程语言是 C,而不是 C++。
  • 这是一个单读单写的案例。
  • 这与生产者-消费者问题不同,因为包装后的 APIget_most_recent_lux()应始终返回最新数据。

由于这是一个单一的读者单一作家案例,我相信:

  • 这里不需要锁。
  • 这里不需要原子值。(所以第 33 行中的 head 没有被声明为原子值,我head = idx在第 40 行使用了正常的评估操作 ( ))。

问:我上面的说法对吗?

面试官一直告诉我,我的陈述并不适用于所有 CPU 架构,所以他认为这里需要互斥锁或原子变量。但我不这么认为。我相信,确实,单行评估 C 代码 ( head = idx) 可以翻译成多条汇编指令,但只有最后一条汇编指令用于将更新后的值存储到内存中。所以,

  • 在最后一条汇编指令执行之前,更新的值还没有更新到内存中,所以阅读器总是会读取旧的头值。
  • 在执行最后一条汇编指令后,阅读器将始终读取更新后的头部值。
  • 在这两种情况下,它都是安全的,不会发生腐败。
  • 没有其他可能性。在只能发生 1 次写入的指定时间段内(假设从 1 变为 2),读取器只能读取 1 或 2,读取器永远不会读取除 1 或 2 之外的任何值,例如 0、3 或1.5。

同意?我真的不敢相信有任何 CPU 拱门代码不起作用。有的话请不吝赐教。非常感谢。

4

1 回答 1

3

您不需要任何原子 RMW 或 seq_cst,但您确实需要_Atomic执行 release-store 和 acquire-load to/from head

这仅在 x86(和 SPARC)上免费发生,而不是其他 ISA,即使针对 x86,与编译时重新排序相比仍然不安全。 可能在更新到之前head = idx;对另一个核心可见,让它读取陈旧的值。records[idx]

(好吧,records[head].lux加载部分实际上可以在大多数 ISA 上工作mo_consume,因为 DEC Alpha 以外的 ISA 保证加载的依赖顺序。)

我认为关于尝试使用非原子变量进行线程间通信的SO还有一些类似的问答。没什么意义,只需使用atomic_store_explicitwith memory_order_release- 它将在 x86 上编译为非原子存储,但具有编译时间顺序保证。stdatomic.h因此,如果您只使用获取和释放,则不会通过避免来提高效率。(负载除外 - 如果您希望在弱排序 ISA 上没有障碍的实际依赖排序,则必须在弱排序 ISA 的受控条件下使用松弛,因为目前已半弃用消耗,并促进在当前编译器中获取。 ) 请参阅何时将 volatile 与多线程一起使用?更多关于为什么手卷原子工作以及为什么他们没有优势。

另外,请注意,您无法防止队列变满并覆盖尚未读取的值。像这样的 SPSC 队列通常让消费者端更新写入者可以检查的读取索引。

于 2021-01-22T15:21:57.727 回答