Is std::atomic<T> safe with interrupts when std::atomic<T>::is_always_lock_free is false?
Asked Answered
M

1

8

In an embedded (ARM) environment with no OS, if I use interrupts, then is there potential for deadlock using std::atomic<T>? If so, how?

In general, any moment, control can be interrupted to handle an interrupt. In particular, if one were to naively have a mutex and wanted to use it to do a "safe" to a variable, one might lock it, write, and unlock and then elsewhere lock, read, and unlock. But if the read is in an interrupt, you could lock, interrupt, lock => deadlock.

In particular, I have a std::atomic<int> for which is_always_lock_free is false. Should I worry about the deadlock case? When I look at the generated assembly, writing 42 looks like:

bl __sync_synchronize
mov r3, #42
str r3, [sp, #4]
bl __sync_synchronize

which doesn't appear to be locking. The asm for reading the value is similar. Is the (possible) lock for the fancier operations like exchange?

Meurer answered 12/6, 2018 at 15:24 Comment(4)
If __sync_synchronize is only a memory barrier, then this code only protects against multiple cores re-ordering or pre-fetching instructions. As far as I know, it does not protect against interrupts and the generated assembler is then not interrupt safe. __sync_synchronize would have to disable all maskable interrupts for this code to be safe. If it does not, I would call the implementation non-conforming.Lewellen
To rule out that this is yet another C++ hiccup like so many before it, you could try C11 _Atomic int and see if it generates the same assembler.Lewellen
I believe the memory reads and writes are atomic on my processor, and I believe it doesn’t do reordering so I think at least for my purposes, the synchronizes aren’t necessary.Meurer
This is several instructions so it is per definition not atomic.Lewellen
A
3

__sync_synchronize is just a builtin for a full memory barrier. There is no locking involved, so no potential for deadlock as there would be with a mutex and interrupt handler.

What ARM core are you using? On an ARM Cortex-A7 the following prints true for both.

#include <iostream>
#include <atomic>

int main()
{
   std::atomic<int> x;
   std::cout << std::boolalpha << x.is_lock_free() << std::endl;
   std::cout << std::atomic<int>::is_always_lock_free << std::endl;
}

I would expect std::atomic<int> to be implemented without locks most if not all on ARM, and certainly from the assembly you provided it does not appear to be using a lock.

Approximation answered 12/6, 2018 at 16:5 Comment(8)
I believe it's a Coretex M0+. The actual variable is a std::atomic<int32_t> p; and if I do static_assert(decltype(p)::is_always_lock_free); I hit that static_assert at compile time. (The assembly example was actually from godbolt.org with ARM gcc 7.2.1 (none).)Meurer
Pretty sure using gobolt is no good as 'ARM' covers quite a range. You really need to look at what is generated for your specific chip. But if its Cortex M0, I think is is ARMv6-M. Reading infocenter.arm.com/help/topic/com.arm.doc.ddi0419d/… and A3.5.1 and "Single-copy atomicity" I see no reason why this would not be always lock free.Approximation
This is about embedded. "mainstream" means anything from S08 to x64. The lattter is unlikely to be used bare-metal (unless we talk about BIOS). You can't assume anything. Almost all 8/16 bit MCUs and quite some larger, too would have to use some way of locking for int operations. The rest could do with retry loops, but that might be even worse, depending on project.Couchant
@Olaf In case of S08 and its likes, it can perform int operations atomic to some extent. But you simply can't trust C code like int a, b; ... a = b; to be atomic. You'd have to either disable interrupts or write inline assembler to be 100% sure.Lewellen
@Lundin: That should have been directed to OP, not to me, I think that's basically what I wrote. (and, for S08 it depends on the specific operation, etc). Nevertheless just don't rely the compiler will emit this code. But that was not the point, S08 just came into mind as a small MCU. Anyway, thanks for providing additional details OP has edited the answer in the meantime. It's still problematic. ARM won't use locks, but could require multiple iterations using exclusive load/stores, which are potentially more costly than a short interrupt lock.Couchant
@Lundin, I realize int a, b; ... a = b; isn’t atomic, but what about std::atomic<int> a, b; ... a = b;? (Or perhaps simpler: std::atomic<int> a; int b; ... a = b; since in my case I know nobody else knows about b.) And if I do temporarily disable interrupts (with RAII since I don’t trust myself), don’t I run the risk of missing an interrupt? (Right now it is using the interrupt as a clock of sorts.)Meurer
@Meurer As far as I know, your example with std::atomic should be safe, which is why your posted disassembly looks non-compliant to me. Indeed if you temporary disable interrupts, you risk delaying them or if there are several from the same source, missing some of them.Lewellen
What looks non-compliant about the assembly? As I read it, it syncs to prevent reordering, then stores, which is atomic, then syncs again to prevent reordering. Where is the problem you see?Meurer

© 2022 - 2024 — McMap. All rights reserved.