Does C++ std::atomic Work with Dual Core ESP32?

So, if I have say:

std::atomic<int16_t> _currentValue;

Will a read/modify/write operation be atomic and free from meddling by tasks and interrupts in either core?

_currentValue += 100;

Also, if an atomic variable is used by by ISR and non-ISR code, does it need to be declared volatile?

volatile std::atomic<int16_t> _currentValue;

Also, is there such a thing as atomic Fetch / Set where the variable is atomically set before returning its previous value?

I suppose the alternative to atomic would be FreeRTOS critical sections. But, I just figured the overhead from using mutexs would be greater.

Thanks.

gfvalvo:
So, if I have say:

std::atomic<int16_t> _currentValue;

Will a read/modify/write operation be atomic and free from meddling by tasks and interrupts in either core?

_currentValue += 100;

I certainly hope so. Otherwise I have a lot of code to fix :slight_smile:
They seem to be using std::atomics in the ESP-IDF tests, so I have no reason to think their atomic implementation wouldn't work.

As a side note, atomic<int16_t> could be slower than atomic, maybe even requiring a lock.

The default operations on atomics (e.g. operator+=) use the most strict memory ordering (sequentially consistent), which migh not be what you want. It's best to be explicit about the memory order.

gfvalvo:
Also, if an atomic variable is used by by ISR and non-ISR code, does it need to be declared volatile?

volatile std::atomic<int16_t> _currentValue;

No, there's no reason to mark atomics as volatile. Volatile has nothing to do with atomicity, but atomics will prevent incorrect compiler optimizations that volatile was meant to avoid.
Edit: I might be wrong here, let me look it up.
Edit2: looked it up, but couldn't find a definitive answer, although most examples I found did not use volatile for std::atomics shared with ISRs.
You can't use volatile for synchronization in a multicore system, you need atomics.

You might want to be careful that your atomics are lock-free if you're using them inside of an ISR, though.

gfvalvo:
Also, is there such a thing as atomic Fetch / Set where the variable is atomically set before returning its previous value?

std::atomic::exchange()?

gfvalvo:
I suppose the alternative to atomic would be FreeRTOS critical sections. But, I just figured the overhead from using mutexs would be greater.

Yes, critical sections require locks and are significantly slower than atomics.

Pieter

PieterP:
The default operations on atomics (e.g. operator+=) use the most strict memory ordering (sequentially consistent), which migh not be what you want. It's best to be explicit about the memory order.

Thanks. For me, it's probably worth taking the performance hit of using the default just so I DON'T have to read and try to decipher all that cryptic memory order meshugaas.

gfvalvo:
Thanks. For me, it's probably worth taking the performance hit of using the default just so I DON'T have to read and try to decipher all that cryptic memory order meshugaas.

It certainly takes some time to wrap your head around it.
I'm not familiar with the details of the memory barriers etc. of the ESP32, it might not even matter for performance.

You'll probably want to know some of the details about memory ordering to understand and reason about how data propagates through the program, but if you just use the default, I think you should be safe.

I edited my previous post. I couldn't find a reliable source and I don't feel like digging in the standard tonight. You can use volatile atomics to be on the safe side, but looking at some examples online, I don't think it's required.

Thanks again.

Would the lack of awareness of an atomic to the environment being used, brings me to wonder about how thread safe, yes, I understand that atomics are supposed to be thread safe, would be with an ESP32?

My thinking comes from the use of malloc on the ESP32. The ESP32 has 3 memory locations for program use. Memory allocated to core0, memory allocated to core1 memory allocated to being shared between core0 and core1.

malloc() is, of course, safe to use inside a task already assigned to a core.

Variables, for the ESP32, are assigned to the memory core that is active when the variables are created. Typically, global variables are assigned to core1, setup() and loop() are defaulted to core1. If malloc() is used in setup() to create a memory allocation for a task that will be ran on core0, the allocation of memory will be to core1 or shared memory. When the task on core0 runs and uses the memory assigned by malloc on core1, all kinds of issues arise. Thus the use of malloc() is discouraged on the ESP32. Instead ESPRESSIF has wrote wrappers, ps_malloc().


Atomics references in the API

It may be noted that freeRTOS makes use of atomics.

I say the following with the proviso that I've done very little testing with it. But the following seems to work:

  • Create an object using "new" in a task running on Core 1.

  • Pass the pointer to the new object (via a queue) to a tasking running on Core 0.

  • Process the object's data in the Core 0 task.

  • "Delete" the object in the Core 0 task.

Like I said, very little testing. But, it looks good so far.

My use case is very simple. Once the Core 1 task creates the object and stuffs its pointer in to the queue, it never touches it again. And, the Core 0 task simply processes the data then destroys the object.

Of course, swapping Core 0 and Core 1 in the above description also works.

YMMV

Idahowalker:
Would the lack of awareness of an atomic to the environment being used, brings me to wonder about how thread safe, yes, I understand that atomics are supposed to be thread safe, would be with an ESP32?

?

Idahowalker:
My thinking comes from the use of malloc on the ESP32. The ESP32 has 3 memory locations for program use. Memory allocated to core0, memory allocated to core1 memory allocated to being shared between core0 and core1.

malloc() is, of course, safe to use inside a task already assigned to a core.

Variables, for the ESP32, are assigned to the memory core that is active when the variables are created. Typically, global variables are assigned to core1, setup() and loop() are defaulted to core1. If malloc() is used in setup() to create a memory allocation for a task that will be ran on core0, the allocation of memory will be to core1 or shared memory. When the task on core0 runs and uses the memory assigned by malloc on core1, all kinds of issues arise. Thus the use of malloc() is discouraged on the ESP32. Instead ESPRESSIF has wrote wrappers, ps_malloc().

Where did you get that information? There's nothing wrong with using malloc on ESP32.
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/mem_alloc.html#thread-safety

Thread Safety
Heap functions are thread safe, meaning they can be called from different tasks simultaneously without any limitations.

Because ESP32 uses multiple types of RAM, it also contains multiple heaps with different capabilities. A capabilities-based memory allocator allows apps to make heap allocations for different purposes.

For most purposes, the standard libc malloc() and free() functions can be used for heap allocation without any special consideration.

ps_malloc() is an Arduino ESP32 Core function to allocate memory on an external SPI RAM chip, so I don't see how that's relevant here.

Idahowalker:
Atomics references in the API

Atomics are part of the C and C++ languages, it's not part of and not documented in the ESP-IDF API.

Idahowalker:
It may be noted that freeRTOS makes use of atomics.

It's of course impossible to write an operating system without atomic operations, but FreeRTOS doesn't use C or C++ atomics. The Atomics library was introduced in C++11 and ported to C11 as well. FreeRTOS predates C11, it has its own set of macros that implement atomic operations, see atomic.h.

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.