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.