Interoperabilty between C and C++ atomics

Viewed 1813

Suppose, I have a task that might be cancelled from another thread. The task is performed in a C function, another thread runs C++ code. How do I do that?

Rough example.

C:

void do_task(atomic_bool const *cancelled);

C++:

std::atomic_bool cancelled;
…
do_task(&cancelled);

For now, I created a file atomics.h with the following content:

#ifdef __cplusplus
#include <atomic>
using std::atomic_bool;
#else
#include <stdatomic.h>
#endif

It appears to work, but I don't see any guarantees for that. I wonder, if there is a better (correct) way.

5 Answers

The atomic_bool type in C and the std::atomic<bool> type in C++ (typedefed as std::atomic_bool) are two different types that are unrelated. Passing a std::atomic_bool to a C function expecting C's atomic_bool is Undefined Behavior. That it works at all is a combination of luck and the simple definitions of these types being compatible.

If the C++ code needs to call a C function that expects C's atomic_bool, then that is what it must use. However, the <stdatomic.h> header does not exist in C++. You'll have to provide a way for the C++ code to call C code to get a pointer to the atomic variable you need in a way that hides the type. (Possibly declare a struct that holds the atomic bool, that C++ would only know that the type exists and only know about pointers to it.)

To side-step all ABI issues you may like to implement a C function that is called from C++ and operates on that atomic_bool. This way your C++ code doesn't need to know anything about that global variable and its type:

In an .hfile:

#ifdef __cplusplus
extern "C" {
#endif

void cancel_my_thread(void);
int is_my_thread_cancelled(void);

#ifdef __cplusplus
}
#endif

And then in a .c file:

#include <stdatomic.h>

static atomic_bool cancelled = 0;

void cancel_my_thread(void) {
    atomic_store_explicit(&cancelled, 1, memory_order_relaxed);
}
int is_my_thread_cancelled(void) {
    return atomic_load_explicit(&cancelled, memory_order_relaxed);
}

The C++ code would include that headed and call cancel_my_thread.

I found this on a net search https://developers.redhat.com/blog/2016/01/14/toward-a-better-use-of-c11-atomics-part-1/

Following the lead of C++, along with a memory model describing the requirements and semantics of multithreaded programs, the C11 standard adopted a proposal for a set of atomic types and operations into the language. This change has made it possible to write portable multi-threaded software that efficiently manipulates objects indivisibly and without data races. The atomic types are fully interoperable between the two languages so that programs can be developed that share objects of atomic types across the language boundary. This paper examines some of the trade-offs of the design, points out some of its shortcomings, and outlines solutions that simplify the use of atomic objects in both languages.

I am just learning about atomics now, but it looks like its compatible between C and CPP.

EDIT

Another source Multi-Threading support in c11

how i understand your code in general is (must be) next

// c code

void _do_task();

void do_task(volatile bool *cancelled)
{
  do {
    _do_task();
  } while (!*cancelled);
}

// c++ code

volatile bool g_cancelled;// can be modify by another thread
do_task(&cancelled);

void some_proc()
{
  //...
  g_cancelled = true;
}

i be ask question - are here we need declare cancelled as atomic ? are we need atomic here ?

atomic need in 3 case:

  • we do Read-Modify-Write operation. say if we need set cancelled to true and check are it was already true. this for example can be need if several threads set cancelled to true and who do this first need free some resources.

    if (!cancelled.exchange(true)) { free_resources(); }

  • the read or write operation for type need to be atomic. of course on all current and all possible future implementations this is true for bool type (despite formal not defined). but even this is not important. we here check cancelled only for 2 values - 0 (false) and all another. so even if both write and read operation on cancelled assume not atomic, after one thread write non-zero to canceled, another thread sooner or later will read modified non-zero value from canceled . even if it will be another value, not the same first thread write: for example if cancelled = true translated to mov cancelled, -1; mov cancelled, 1 - two hardware, not atomic operation - second thread can read -1 instead final 1 (true) from canceled, but this not play role if we check only for non-zero - all another values break loop - while (!*cancelled); if we use here atomic operation for write/read cancelled - nothing change here - after one thread atomic write to it another thread sooner or later will read modified non-zero value from canceled - atomic operation or not - memory is common - if one thread write to memory (atomic or no) another threads sooner or later will view this memory modification.

  • we need synchronize another read/writes with cancelled. so we need synchronization point between 2 threads around canceled with memory order other than memory_order_relaxed say for example next code:

//

void _do_task();

int result;

void do_task(atomic_bool *cancelled)
{
    do {
        _do_task();
    } while (!g_cancelled.load(memory_order_acquire));

    switch(result)
    {
    case 1:
        //...
        break;
    }
}

void some_proc()
{
    result = 1;
    g_cancelled.store(true, memory_order_release);
}

so we not simply set g_cancelled to true here, but before this
write some shared data (result) and want that another thread after view modification of g_cancelled, will be also view modification of
shared data (result). but i doubt that you actually use/need this
scenario

if none of this 3 things is need- you not need atomic here. what you really need - that one thread just write true to cancelled and another thread all time read value of cancelled (instead do this once and cache result). usual in most case of code this will be done auto, but for be exactly you need declare canceled as volatile

if however you by some reason need exactly atomic (atomic_bool), because you here cross the border of languages, you need understand concrete implementation of atomic_bool in both languages and are it the same (type declaration, operations (load, store, etc)). by fact atomic_bool is the same for c and c++.

or (better) instead of make visible and share type atomic_bool use interface functions like

bool is_canceled(void* cancelled);

so code can be next

// c code
void _do_task();

bool is_canceled(void* cancelled);

void do_task(void *cancelled)
{
    do {
        _do_task();
    } while (!is_canceled(cancelled));
}

// c++ code

atomic_bool g_cancelled;// can be modify by another thread

bool is_canceled(void* cancelled)
{
    return *reinterpret_cast<atomic_bool*>(cancelled);
}

void some_proc()
{
    //...
    g_cancelled = true;
}

do_task(&g_cancelled);

but again i doubt that in your task you need atomic_bool by semantic. you need volatile bool

Atomicity of operations is caused by hardware, not software (well, in C++ there are also "atomic" variables that are atomic in name only, those are implemented via mutexes and locks). So, basically, C++ atomics and C atomics do the very same thing. Hence as long as the types are compatible there won't be issues. And C++11 and C11 atomic classes were made to be compatible.


Apparently, people do not understand how atomics and locks work and require further explanation. Check out current memory models for more information.

1) We will start with basics. What and why are atomics? How does memory works?

Memory Model: think of processor as several independent cores and each has its own memory (cashes L1, L2, and L3; and in fact, L3 cash is common but it isn't really important).

Why do we need atomic operation?

If you don't use atomics, then each processor might have its own version of the variable 'x' and they are in general not synchronized. There is no telling when they will perform synchronizations with RAM/L3 cash.

When atomic operations are used, such memory operations are used that ensure synchronization with RAM/L3 cash (or whatever is needed) - ensuring that different cores have access to the same variable and not have variety of different versions of it.

Nobody cares if it is C, C++, or whatever language you use - as long as one ensures memory synchronization (both read, write, and modify) there will never be no issues.

2) OK, what about locks and mutexes?

Mutexes tend to work with OS and have queue over which thread should be allowed next to perform. And they enforce stricter memory synchronization than atomics do. With atomics one can syhchronize just the variable itself or more depending on the request / which function you call.

3) Say I have atomic_bool, can it work in interchangeably on different languages (C/C++11)?

Normally a boolean can be sychronized via memory operations (you're just synchronizing a single byte of memory from their perspective). If the compilers are aware that the hardware can perform such operations then they surely will use them as long as you use the standard.

Logical atomics (any std::atomic< T > with T having wrong size/alignment) are synchronized via locks. In this case it is unlikely that different languages can use them interchangeably - if they have different methods of usage of these locks, or for some reason one decided to use a lock and the other one came to conclusion that it can work with atomic hardware memory synchronizations... then there will be issues.

If you use atomic_bool on any modern machine with C/C++, it will surely be able to synchronize without locks.

Related