我没有找到太多关于非原子操作的材料。
假设我有一个 32 位处理器,并且我想在 64 位变量中保留微秒计数。中断将每微秒更新一次变量。调度程序是非抢占式的。将有一个函数用于清除变量,另一个函数用于读取变量。由于它是 32 位处理器,因此访问将是非原子的。是否有一种“标准”或惯用的方法来处理这个问题,以便读取器函数不会获得半更新的值?
I am not finding much material on non-atomic operations.
Suppose I have a 32 bit processor and I want to keep count of microseconds in a 64 bit variable. An interrupt will update the variable every microsecond. The scheduler is non-preemptive. There will be a function to clear the variable and another to read it. Since it is a 32 bit processor then access will be non-atomic. Is there a “standard” or idiomatic way of handling this so that the reader function will not get a half-updated value?
发布评论
评论(3)
您需要做的是使用我所说的“原子访问防护”或“中断防护”。这是我感兴趣的领域,我花费了大量时间来学习和使用各种类型的微控制器。
@chux - 恢复莫妮卡,是正确的,但我想补充一些说明:
供阅读
通过快速复制变量,然后在计算中使用副本,最大限度地缩短中断关闭的时间:
对于写入易失性变量,快速写入:
通过在更新易失性变量时快速禁用中断,最大限度地缩短中断关闭的时间:
替代方案:通过重复读取进行无锁原子读取循环:
doAtomicRead()
:确保原子读取而不关闭中断!如上所示,使用原子访问防护的另一种方法是重复读取变量,直到它不再更改,这表明该变量未在读取后的中间更新您只读取了其中的一些字节。
请注意,这适用于任何大小的内存块。下面的示例中使用的 uint64_t 类型甚至可以是几十个或几百个字节的 struct my_struct 。它不限于任何尺寸。
doAtomicRead()
仍然有效。这是该方法。 @Brendan 和 @chux-ReinstateMonica 和我在 @chux-ReinstateMonica 的回答下讨论了一些想法。
如果您想更深入地了解,这里又是相同的
doAtomicRead()
函数,但这次带有大量解释性注释。我还展示了一个注释掉的细微变化,这在某些情况下可能会有所帮助,如评论中所述。通过在循环开始之前添加一次额外的读取,可以优化上述内容,以仅获得
*val
的新读数每次迭代仅获得一次,而不是两次,并且在循环中只读取一次,如下所示:[这是我最喜欢的版本:]
一般用法
doAtomicRead()
的示例:这要求编写器在以下方面具有原子性:任何读者,例如,对于单个作者而言,这是正确的写入该变量。例如,此写入可能发生在 ISR 内部。我们仅检测到撕裂的读取(由于读取器被中断)并重试。如果当该读取器运行时 64 位值在内存中已经处于撕裂写入状态,则读取器可能会错误地将其视为有效。
SeqLock 没有这个限制,因此对于多核情况很有用。但是,如果您不需要它(例如:您有一个单核微控制器),它的效率可能会较低,而
doAtomicRead()
技巧就可以正常工作。对于单调递增计数器的特殊边缘情况(不适用于可以用任何值更新的变量,例如存储传感器读数的变量!),正如布伦丹在这里建议的您只需要重新读取 64 位值的最高有效一半并检查它是否没有 改变。因此,为了(可能)稍微提高上面的 doAtomicRead() 函数的效率,请对其进行更新。唯一可能的撕裂(除非您错过 2^32 计数)是当低半部分换行且高半部分递增时。这就像检查整个事情,但重试的频率会更低。
在 FreeRTOS 中使用
doAtomicRead()
易失性
全局变量,因此它们感觉不像全局变量。这是一种“共享内存”多线程模型。这些变量不需要是原子的。为所有变量提供了 setter 和 getter。只有这一项任务可以写入变量。 它的优先级必须高于读者任务才能正常工作。即:通过成为比消费者更高优先级的任务,它模拟处于受保护的 ISR 中,并且能够相对于读者/消费者以原子方式写入。doAtomicRead()
函数,如上所述。请注意,这适用于任何大小的内存块。我的示例中使用的 uint64_t 类型甚至可能是数十或数百字节的结构。它不限于任何尺寸。进一步讨论原子访问防护、禁用中断等主题。
我的c/containers_ring_buffer_FIFO_GREAT.c 来自我的 eRCaGuy_hello_world 存储库的演示。此代码示例的描述来自我在该文件顶部的注释:
<块引用>
演示基本、高效的无锁 SPSC(单生产者单消费者)环形缓冲区 FIFO
C 中的队列(也在 C++ 中运行)。
此队列仅在 SPSC 上下文中无锁工作,例如在裸机上
例如,ISR 需要将数据发送到主循环的微控制器。
[Peter Cordes 对 SeqLock 的回答(“序列lock") 模式] 用 32 位实现 64 位原子计数器原子
[我的答案] C++ 递减单字节(易失性)数组的元素不是原子的!为什么? (另外:如何在 Atmel AVR mcus/Arduino 中强制原子性)
我又长又详细的答案< /a> 哪些 Arduino 支持 ATOMIC_BLOCK? 以及:
[我的问答]哪些变量类型/大小在 STM32 微控制器上是原子的?
在 STM32 MCU 上禁用中断的技术:https://stm32f4-discovery.net/2015/06/how-to-properly-enabledisable-interrupts-in-arm-cortex-m/
[我的答案] ISR 中未更新全局易失性变量:如何使用原子访问识别和修复 Arduino 中的竞争条件守卫:
[我的答案] 禁用和重新启用中断的各种方法有哪些STM32微控制器为了实现原子访问防护?
https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock
What you need to do is use what I call "atomic access guards", or "interrupt guards". This is an area of interest of mine that I have spent a ton of time learning about and using in microcontrollers of various types.
@chux - Reinstate Monica, is correct, but here's some additional clarity I want to make:
For reading from volatile variables, make copies in order to read quickly:
Minimize time with the interrupts off by quickly copying out the variable, then using the copy in your calculation:
For writing to volatile variables, write quickly:
Minimize time with the interrupts off by quickly only disabling them while updating the volatile variables:
Alternative: lock-free atomic reads via a repeat read loop:
doAtomicRead()
: ensure atomic reads withOUT turning interrupts off!An alternative to using atomic access guards, as shown above, is to read the variable repeatedly until it doesn't change, indicating that the variable was not updated mid-read after you read only some bytes of it.
Note that this works on memory chunks of any size. The
uint64_t
type used in my examples below could instead be astruct my_struct
of dozens or hundreds of bytes even. It is not limited to any size.doAtomicRead()
still works.Here is that approach. @Brendan and @chux-ReinstateMonica and I discussed some ideas of it under @chux-ReinstateMonica's answer.
If you want to understand deeper, here is the same
doAtomicRead()
function again, but this time with extensive explanatory comments. I also show a commented-out slight variation to it which may be helpful in some cases, as explained in the comments.The above could be optimized to only obtain a new reading of
*val
only once per iteration, instead of twice, by adding one extra read before the start of the loop, and reading only once in the loop, like this:[This is my favorite version:]
General usage Example of
doAtomicRead()
:This requires the writer to be atomic with respect to any readers, which is true, for example, in the case of a single writer writing to this variable. This write might occur inside an ISR, for example. We're detecting only torn reads (due to the reader being interrupted) and retrying. If the 64-bit value was ever already in a torn written state in memory when this reader ran, the reader could erroneously see it as valid.
A SeqLock doesn't have that limitation, so is useful for multi-core cases. But, if you don't need that (ex: you have a single-core microcontroller), it is probably less efficient, and the
doAtomicRead()
trick works just fine.For the special edge case of a monotonically-incrementing counter (not for a variable which can be updated with any value, such as a variable storing a sensor reading!), as Brendan suggested here you only need to re-read the most-significant half of the 64-bit value and check that it didn't change. So, to (probably) slightly improve the efficiency of the
doAtomicRead()
function above, update it to do that. The only possible tearing (unless you miss 2^32 counts) is when the low half wraps and the high half gets incremented. This is like checking the whole thing, but retries will be even less frequent.Using
doAtomicRead()
in FreeRTOSvolatile
global variables encapsulated in a module, so they don't feel like global variables. This is a "shared memory" multi-threaded model. These variables are not required to be atomic. Setters and getters are provided for all variables. ONLY this one task can write into the variables. It must be a higher-priority than the reader tasks for this to work properly. ie: by being a higher-priority task than the consumers, it emulates being in a protected ISR, and is able to write atomically with respect to the readers/consumers.doAtomicRead()
function as described above. Note that this works on memory chunks of any size. Theuint64_t
type used in my example could be a struct of dozens or hundreds of bytes even. It is not limited to any size.Going further on this topic of atomic access guards, disabling interrupts, etc.
My c/containers_ring_buffer_FIFO_GREAT.c demo from my eRCaGuy_hello_world repo. Description of this code example from my comments at the top of this file:
[Peter Cordes's answer on the SeqLock ("sequence lock") pattern] Implementing 64 bit atomic counter with 32 bit atomics
[my answer] C++ decrementing an element of a single-byte (volatile) array is not atomic! WHY? (Also: how do I force atomicity in Atmel AVR mcus/Arduino)
My long and detailed answer on Which Arduinos support ATOMIC_BLOCK? and:
[my Q&A] Which variable types/sizes are atomic on STM32 microcontrollers?
Techniques to disable interrupts on STM32 mcus: https://stm32f4-discovery.net/2015/06/how-to-properly-enabledisable-interrupts-in-arm-cortex-m/
[my answer] global volatile variable not being updated in ISR: How to recognize and fix race conditions in Arduino by using atomic access guards:
[my answer] What are the various ways to disable and re-enable interrupts in STM32 microcontrollers in order to implement atomic access guards?
https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock
在 ISR 内,通常会阻止后续中断(除非优先级更高,但通常不会触及计数),因此只需
count_of_microseconds++;
在 ISR 外部,访问(读或写)< code>count_of_microseconds 你需要中断保护或原子访问。
当
atomic
不可用*1但解释控制可用时:否则使用
请参阅如何在 C 中使用原子变量?
从 C89 开始,将
volatile
与count_of_microseconds
一起使用。[更新]
无论非 ISR 代码中使用哪种方法(此答案或其他)来读/写计数器,我建议将读/写代码包含在辅助函数中以隔离这组关键操作。
*1
自 C11 起可用,__STDC_NO_ATOMICS__
未定义。Within the ISR, a subsequent interrupt is usually prevented (unless a higher priority, but then the count is usually not touched there) so simply
count_of_microseconds++;
Outside the ISR, to access (read or write) the
count_of_microseconds
you need interrupt protection or atomic access.When
atomic
not available*1 but interpret control is available:else use
See How to use atomic variables in C?
Since C89, use
volatile
withcount_of_microseconds
.[Update]
Regardless of the approach used (this answer or others) in the non-ISR code to read/write the counter, I recommend to enclose the read/write code in a helper function to isolate this critical set of operations.
*1
<stdatomic.h>
available since C11 and__STDC_NO_ATOMICS__
not deifned.我很高兴听到读两次方法是可行的。我有疑问,不知道为什么。与此同时,我想出了这个:
I am glad to hear the that the read twice method is workable. I had doubts, don't know why. In the meantime I came up with this: