Atomic Counters in Chapel

Our primary mechanism for managing state in Chapel is through synchronization variables and atomic operations. In this example, we’ll look at using atomic operations for atomic counters accessed by multiple tasks.

use Time;

proc main() {
    // We'll use an atomic integer to represent our (always-positive) counter.
    var ops: atomic int;

    // We'll start 50 tasks that each increment the counter exactly 1000 times.
    coforall i in 1..50 {
        for c in 1..1000 {
            // To atomically increment the counter we use fetchAdd.
            ops.fetchAdd(1);
        }
    }

    // Here no tasks are writing to 'ops', but it's safe to read the value
    // even while other tasks might be updating it.
    writeln("ops: ", ops.read());
}

To run the program:

$ chpl atomic_counters.chpl
$ ./atomic_counters
ops: 50000

We expect to get exactly 50,000 operations. If we had used a non-atomic integer and incremented it with ops += 1, we’d likely get a different number, changing between runs, because the tasks would interfere with each other.

In Chapel, we use the coforall loop to create multiple concurrent tasks. Each task increments the counter 1000 times using the atomic fetchAdd operation. The atomic type ensures that these operations are performed atomically, preventing race conditions.

After all tasks complete (which happens automatically at the end of the coforall loop), we read and print the final value of the counter using the read method of the atomic variable.

This approach ensures thread-safety and provides a reliable way to perform concurrent operations on shared data.

Next, we’ll look at synchronization variables, another tool for managing state in Chapel.