Issue
I'm using C11 atomics on a GNU compile embedded system (basically the same as std::atomics). I'm having trouble finding use cases for them, even in a very simple example I'm working on. Compare the following very simple program design: I want threadA to always write and threadB to read the information stored in a shared C-string.
First try:
#include <string.h>
#include <cstdlib>
#define CONFIGURED_MAX 128
static char* shared_ptr_to_str;
char* get_that_char()
{
char* str = NULL;
__atomic_load(shared_ptr_to_str, str, __ATOMIC_ACQUIRE);
return str;
}
void threadA()
{
char some[] = "Some String we got over network somehow";
char * tmp_ptr = (char*) malloc(CONFIGURED_MAX);
strncpy(tmp_ptr, some, CONFIGURED_MAX);
__atomic_store(shared_ptr_to_str, tmp_ptr, __ATOMIC_RELEASE);
}
void threadB()
{
char* grabbed_str = get_that_char();
// use grabbed_str somehow
}
This approach already has various problems:
- Obviously it doesn't free the memory previously used to store the string when threadA iterates.
- I'm holding on to a string in threadB() which could change at any moment.
When I try to fix those problems we arrive at situation B
Second try:
#include <string.h>
#include <cstdlib>
#define CONFIGURED_MAX 128
static char* shared_ptr_to_str;
void cpy_that_char(char** to_fill)
{
char *str = NULL;
__atomic_load(shared_ptr_to_str, str, __ATOMIC_ACQUIRE);
*to_fill = (char*) malloc(strnlen(str, CONFIGURED_MAX));
strncpy(*to_fill, str, CONFIGURED_MAX);
}
void threadA()
{
char some[] = "Some String we got over network somehow";
// free old buffer first
free(shared_ptr_to_str);
// fill in new stuff
char * tmp_ptr = (char*) malloc(strnlen(some, CONFIGURED_MAX));
strncpy(tmp_ptr, some, strnlen(some, CONFIGURED_MAX));
__atomic_store(shared_ptr_to_str, tmp_ptr, __ATOMIC_RELEASE);
}
void threadB()
{
char* grabbed_str = NULL;
cpy_that_char(&grabbed_str);
// use grabbed_str somehow
}
Now I have made it even worse! Although I now get a local copy of the string for threadB (so it can do with it whatever it pleases), the atomic operations could still interfere with each other:
- In threadA in between freeing and reassigning memory, cpy_that_char() could be called from threadB and encounter freed memory.
- In function cpy_that char, strnlen() is already not guaranteed anymore to encounter the same shared_ptr_to_str when it is called after atomic load. The memory address could have been freed in between by threadA()
This means that I have to group the calls together: free() should be grouped with store in threadA and load should be grouped with strnlen() in cpy_that_char() from threadB.
And this basically means we're back at mutexes...
In almost any case I encountered a situation like this. It looks at first like it could be solved with atomics, but I fall back to mutexes again and again. Can anyone tell me what a real use case of atomics is and if I could have solved above example with atomics?
Solution
Atomics are useful for primitives. For example, if thread A is processing items, and thread B is reporting progress, then thread A could write out the number of items processed into an atomic integer, and thread B could read it without needing a mutex.
They're also useful if there are multiple writers to the same value. In the example above, the progress counter could be atomically incremented from multiple processing threads.
Another use case is a "quit" flag, written from the main thread and read regularly from worker threads to check if they should exit.
In your case, where the data is not a primitive, there is nothing wrong with using a mutex. I suspect that any attempt to write it using atomics only would just end up reinventing a mutex anyway.
Answered By - Thomas