Search code examples
cmultithreadingarchitectureoperating-system

When to use atomic operations while programming?


I knew that cpu provides atomic instructions to atomically access a specified memory address. I'm curious about when should we use atomic instructions. More extractly, some experts told me that "we should use atomic when more than two threads access the shared variable". My question is that what if I have a reading thread with a writing thread? For example, I have non-atomic code snippets below:

#define READY 1
#define NOTREADY 0

int flag;
int msg;

// thread 1
void reader() {
    while (flag == NOTREADY) ;  // not-atomic operation

    process(msg);

    flag = NOTREADY;
}

// thread 2
void writer() {
    while (1) {
        while (flag == READY) ;
        msg = send_msg();
        flag = READY;  // not-atomic operation
    }
}

In this example, thread 1 enters spinloop until flag is NOTREADY, while thread 2 will send_msg and set flag to READY. What if I don't make accesses to flag atomic operation? Would these codes run well?

I tried to run code below and the result is ok.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

#define READY 1
#define NOTREADY 0

int msg, flag = NOTREADY, cnt = 0;

void process(int msg) {
  printf("Processing msg: %d\n", msg);
}

void *reader() {
  while (cnt < 200000) {
    while (flag == NOTREADY) ;

    process(msg);

    flag = NOTREADY;
  }
}

int send_msg() {
  cnt ++;
  printf("Sending msg: %d\n", cnt);
  return cnt;
}

void *writer() {
  while (cnt < 200000) {
    while (flag == READY) ;

    msg = send_msg();

    flag = READY;
  }
}

int main() {
  pthread_t r_id, w_id;
  pthread_create(&r_id, NULL, reader, NULL);
  pthread_create(&w_id, NULL, writer, NULL);

  pthread_join(r_id, NULL);
  pthread_join(w_id, NULL);

  return 0;
}

So could we omit atomic operation when the program has only one thread for reading and one thread for writing to the shared variable? If we can't, why not?


Solution

  • In this case, omitting atomic operations is working, but it is not guaranteed to always work.

    The reason is that the compiler and CPU are optimizing your code in a way that happens to work in this simple example, but it is not guaranteed behavior.

    The potential issues are:

    Instruction reordering - The CPU may reorder the instructions within a thread in a way that affects the other thread. For example, it may move the flag = READY statement before the msg = send_msg() statement. If this happens, the reader thread could see flag as READY before msg is actually updated.

    Caching - The CPU may cache the value of flag in a register for performance. So the writer thread may update flag, but the reader thread continues to see the old cached value.

    By using atomic operations, you disable these optimizations and ensure that:

    Instructions are executed in program order Writes to shared memory are visible to other threads immediately. So in summary, for simple examples like this, omitting atomics may work. But for robust, portable code that works on all platforms, you should use atomic operations when accessing shared variables from multiple threads.