I have heard that it is undefined behavior to read/write to the same location in memory concurrently, but I am unsure if the same is true when there are no clear race-conditions involved. I suspect that the c18 standard will state it is undefined behavior on principal due to the potential to create race conditions, but I am more interested in if this still counts as undefined behavior at an application level when these instances are surrounded by fencing.
Setup
For context, say we have two threads A and B, set up to operate on the same location in memory. It can be assumed that the shared memory mentioned here is not used or accessible anywhere else.
// Prior to the creation of these threads, the current thread has exclusive ownership of the shared memory
pthread_t a, b;
// Create two threads which operate on the same memory concurrently
pthread_create(&a, NULL, operate_on_shared_memory, NULL);
pthread_create(&b, NULL, operate_on_shared_memory, NULL);
// Join both threads giving the current thread exclusive ownership to shared memory
pthread_join(a, NULL);
pthread_join(b, NULL);
// Read from memory now that the current thread has exclusive ownership
printf("Shared Memory: %d\n", shared_memory);
Write/Write
Each thread then theoretically runs operate_on_shared_memory
which mutates the value of shared_memory
at the same time across both threads. However with the caveat that both threads attempt to set the shared memory to the same unchanging constant. Even if it is a race condition, the race winner should not matter. Does this count as undefined behavior? If so, why?
int shared_memory = 0;
void *operate_on_shared_memory(void *_unused) {
const int SOME_CONSTANT = 42;
shared_memory = SOME_CONSTANT;
return NULL;
}
Optional Branching Write/Write
If the previous version does not count as undefined behavior, then what about this example which first reads from shared_memory
then writes the constant to a second location in shared memory. The important part here being that even if one or both threads succeeds in running the if statement, it should still have the same outcome.
int shared_memory = 0;
int other_shared_memory = 0;
void *operate_on_shared_memory(void *_unused) {
const int SOME_CONSTANT = 42;
if (shared_memory != SOME_CONSTANT) {
other_shared_memory = SOME_CONSTANT;
}
shared_memory = SOME_CONSTANT;
return NULL;
}
If this is undefined behavior, then why? If the only reason is that it introduces a race condition, is there any reason why I shouldn't deem it acceptable for one thread to potentially execute an extra machine instruction? Is it because the CPU or compiler may re-order memory operations? What if I were to put atomic_thread_fence
at the start and end of the operate_on_shared_memory
?
Context
GCC and Clang doesn't seem to have any complaints. I used c18 for this test, but I don't mind referring to a later standard if they are easier to reference.
$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
$ gcc -std=c18 main.c -pthread -g -O3 -Wall