问题描述
在今天的面试中被要求将阻塞调用封装到非阻塞调用。 所以我们(面试官和我)决定通过在非阻塞 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。 - 在内部,它启动一个线程来执行第 36 行定义的
worker_thread()
函数。 -
worker_thread()
不断调用阻塞调用并将数据写入 ringbuf。 - 这样读者就可以从 ringbuf 中读取最近的记录数据。
另外请注意:
- 这里使用的编程语言是 C,而不是 C++。
- 这是一个单读取器单写入器案例。
- 这与生产者-消费者问题不同,因为封装的 API
get_most_recent_lux()
应始终返回最新数据。
由于这是一个单读者单作者案例,我相信:
- 此处无需锁定。
- 此处不需要原子值。
(所以第 33 行的头部没有被声明为原子值,
并且我在第 40 行使用了正常的评估操作 (
head = idx
)。
问题:我上面的说法正确吗?
面试官一直跟我说我的说法对所有cpu架构都不正确,所以他认为这里需要互斥或原子变量。
但我不这么认为。
我相信,确实,单行评估 C 代码(head = idx
)可以转换为多条汇编指令,但只有最后一条汇编指令用于将更新的值存储到内存中。所以,
- 在最后一条汇编指令执行之前,更新的值还没有更新到内存中,所以读取器总是读取旧的头部值。
- 在执行最后一条汇编指令后,读取器将始终读取更新后的 head 值。
- 在这两种情况下,都是安全的,不会发生损坏。
- 没有其他可能性。在只能发生 1 次写入的指定时间段内(假设从 1 更改为 2),读取器只能读取 1 或 2,读取器永远不会读取除 1 或 2 以外的任何值,例如 0、3 或1.5.
同意吗? 我真的不敢相信有任何代码无法运行的 cpu 架构。 如果有,请教教我。 非常感谢。
解决方法
您不需要任何原子 RMW 或 seq_cst,但您确实需要 _Atomic
来执行与 head
之间的发布存储和获取加载。
这仅在 x86(和 SPARC)上免费发生,而不是其他 ISA,并且即使针对 x86,与编译时重新排序相比仍然不安全。 head = idx;
可以在更新 records[idx]
之前对另一个核心可见,让它读取过时的值。
(嗯,records[head].lux
加载部分实际上适用于大多数 ISA,例如 mo_consume
,因为除 DEC Alpha 之外的 ISA 保证加载的依赖顺序。)
我认为关于尝试使用非原子变量进行线程间通信的 SO 上还有其他一些类似的问答。没什么意义,只需将 atomic_store_explicit
与 memory_order_release
一起使用 - 它将在 x86 上编译为非原子存储,但具有编译时排序保证。因此,如果您只使用获取和释放,则避免使用 stdatomic.h
并不会提高效率。 (除了负载——如果你想要在弱排序 ISA 上没有障碍的实际依赖排序,你必须在弱排序 ISA 的受控条件下使用放松,因为消耗目前是半弃用的,并在当前编译器中促进获取。 ) 请参阅 When to use volatile with multi threading? 以了解有关手动原子操作为何有效以及它们为何没有优势的更多信息。
另外,请注意,您无法防止队列变满并覆盖尚未读取的值。像这样的 SPSC 队列通常让消费者端更新写入者可以检查的读取索引。