Atomic Counters in C

Our program demonstrates the use of atomic operations for managing a counter accessed by multiple threads. In C, we’ll use the <stdatomic.h> header for atomic operations and <pthread.h> for multi-threading.

#include <stdio.h>
#include <stdatomic.h>
#include <pthread.h>

// We'll use an atomic unsigned long long to represent our (always-positive) counter.
atomic_ullong ops = 0;

#define NUM_THREADS 50
#define ITERATIONS 1000

// Function to be executed by each thread
void* increment_counter(void* arg) {
    for (int i = 0; i < ITERATIONS; i++) {
        // To atomically increment the counter we use atomic_fetch_add
        atomic_fetch_add(&ops, 1);
    }
    return NULL;
}

int main() {
    pthread_t threads[NUM_THREADS];

    // Start 50 threads that each increment the counter exactly 1000 times.
    for (int i = 0; i < NUM_THREADS; i++) {
        if (pthread_create(&threads[i], NULL, increment_counter, NULL) != 0) {
            perror("Failed to create thread");
            return 1;
        }
    }

    // Wait for all threads to finish.
    for (int i = 0; i < NUM_THREADS; i++) {
        if (pthread_join(threads[i], NULL) != 0) {
            perror("Failed to join thread");
            return 1;
        }
    }

    // Here no threads are writing to 'ops', but using
    // atomic_load it's safe to atomically read a value even while
    // other threads might be (atomically) updating it.
    printf("ops: %llu\n", atomic_load(&ops));

    return 0;
}

To compile and run this program, you would typically use:

$ gcc -o atomic-counters atomic-counters.c -lpthread
$ ./atomic-counters
ops: 50000

We expect to get exactly 50,000 operations. Had we used a non-atomic integer and incremented it with regular addition, we’d likely get a different number, changing between runs, because the threads would interfere with each other. Moreover, we’d get data race conditions, which can lead to unpredictable behavior.

In C, we use pthread_t to represent threads and pthread_create() to start them. The pthread_join() function is used to wait for threads to finish, similar to the WaitGroup in the original example.

The atomic_ullong type is used for the counter, and atomic_fetch_add() is used for atomic incrementation. atomic_load() is used to safely read the final value.

Note that this C code requires a C11-compatible compiler and the -lpthread flag for linking with the POSIX threads library.