Atomic Counters in Perl

Our primary mechanism for managing state in Perl is through shared variables. However, when dealing with concurrent programming, we need to be careful about how we access and modify shared state. In this example, we’ll look at using the threads::shared module for atomic operations on shared counters accessed by multiple threads.

use strict;
use warnings;
use threads;
use threads::shared;
use Time::HiRes qw(usleep);

# We'll use a shared integer to represent our (always-positive) counter.
my $ops : shared = 0;

# We'll start 50 threads that each increment the counter exactly 1000 times.
my @threads;
for my $i (0..49) {
    push @threads, threads->create(sub {
        for my $c (0..999) {
            # To atomically increment the counter we use lock.
            {
                lock($ops);
                $ops++;
            }
            # Simulate some work
            usleep(rand(100));
        }
    });
}

# Wait until all the threads are done.
$_->join for @threads;

# Here no threads are writing to 'ops', but using
# lock it's safe to atomically read a value even while
# other threads might be updating it.
{
    lock($ops);
    print "ops: $ops\n";
}

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

$ perl atomic_counters.pl
ops: 50000

In this Perl implementation:

  1. We use the threads::shared module to create a shared variable $ops that can be safely accessed by multiple threads.

  2. Instead of goroutines, we create 50 Perl threads using the threads module.

  3. We use the lock function to ensure atomic operations on the shared counter. This is equivalent to the atomic.Add operation in Go.

  4. We simulate some work in each iteration using usleep with a random delay.

  5. We join all threads at the end to wait for their completion, similar to the WaitGroup in Go.

  6. Finally, we print the value of $ops, which should be 50,000.

Note that Perl’s threading model is different from Go’s goroutines. Perl threads are more heavyweight and the performance characteristics may differ. Additionally, Perl’s threads are not as widely used for concurrent programming as goroutines are in Go, but they serve a similar purpose in this example.

Next, we’ll look at other synchronization primitives for managing state in concurrent Perl programs.