微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

单读单写固定大小的 ringbuf,没有锁和原子变量,对任何 CPU 架构来说总是安全的吗?

如何解决单读单写固定大小的 ringbuf,没有锁和原子变量,对任何 CPU 架构来说总是安全的吗?

在今天的面试中被要求将阻塞调用封装到非阻塞调用。 所以我们(面试官和我)决定通过在非阻塞 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_explicitmemory_order_release 一起使用 - 它将在 x86 上编译为非原子存储,但具有编译时排序保证。因此,如果您只使用获取和释放,则避免使用 stdatomic.h 并不会提高效率。 (除了负载——如果你想要在弱排序 ISA 上没有障碍的实际依赖排序,你必须在弱排序 ISA 的受控条件下使用放松,因为消耗目前是半弃用的,并在当前编译器中促进获取。 ) 请参阅 When to use volatile with multi threading? 以了解有关手动原子操作为何有效以及它们为何没有优势的更多信息。

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其他元素将获得点击?
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。)
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbcDriver发生异常。为什么?
这是用Java进行XML解析的最佳库。
Java的PriorityQueue的内置迭代器不会以任何特定顺序遍历数据结构。为什么?
如何在Java中聆听按键时移动图像。
Java“Program to an interface”。这是什么意思?