Atomic Counters in D Programming Language

Our primary mechanism for managing state in D is through message passing and synchronization primitives. However, there are other options for managing state as well. In this example, we’ll look at using the core.atomic module for atomic counters accessed by multiple threads.

import std.stdio;
import std.parallelism;
import core.atomic;
import core.thread;

void main()
{
    // We'll use an atomic integer to represent our (always-positive) counter.
    shared long ops;

    // We'll start 50 tasks that each increment the counter exactly 1000 times.
    auto pool = new TaskPool(50);
    scope(exit) pool.finish();

    foreach (_; 0..50)
    {
        pool.put(task({
            for (int c = 0; c < 1000; c++)
            {
                // To atomically increment the counter we use atomicOp!"+="
                atomicOp!"+="(ops, 1);
            }
        }));
    }

    // Wait until all the tasks are done.
    pool.finish(true);

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

We expect to get exactly 50,000 operations. Had we used a non-atomic integer and incremented it with ops++, we’d likely get a different number, changing between runs, because the threads would interfere with each other. Moreover, we might encounter race conditions.

To run the program:

$ dmd -run atomic_counters.d
ops: 50000

In this D version:

  1. We use shared long ops to declare a shared variable that can be safely accessed from multiple threads.

  2. Instead of goroutines, we use D’s std.parallelism module to create a thread pool and spawn tasks.

  3. The atomicOp!"+="function is used to atomically increment the counter, which is equivalent to the Add method in Go.

  4. We use atomicLoad to safely read the final value of the counter, which is similar to the Load method in Go.

  5. The TaskPool and its finish method are used to manage and wait for all tasks to complete, serving a similar purpose to Go’s WaitGroup.

This example demonstrates how to use atomic operations in D to safely manage a counter across multiple threads, achieving the same goal as the original Go program.