Build a Safe Perl Thread Worker with join and Shared State
You will finish with a small Perl ithreads program that starts two workers, collects their return values, and updates one shared counter without a race. The examples match the Perl 5.38.2 installation and perl-doc package used here. Allow about fifteen minutes. You need a shell, Perl with ithreads enabled, and a text editor; no elevated privileges are needed.
The route
Jump straight to the step you need, or tick off Done means at the end.
This is interpreter threading, not a promise of POSIX thread semantics. A new Perl thread gets its own interpreter and a copy of the creating thread's data. Data is private unless you deliberately share it. That default is the first boundary to keep in mind.
1. Check the installed Perl
Thread support is selected when Perl is built. Check the binary before writing a program that depends on it:
$ perl -V:useithreads
useithreads='define';
On this machine, the interpreter is Perl 5.38.2, built for x86_64-linux-gnu-thread-multi. The installed packages are perl-base 5.38.2-3.2ubuntu0.6 and perl-doc 5.38.2-3.2ubuntu0.6. The core modules report threads 2.36 and threads::shared 1.68:
$ perl -Mthreads -e 'print "threads $threads::VERSION\n"'
threads 2.36
$ perl -Mthreads::shared -e 'print "threads::shared $threads::shared::VERSION\n"'
threads::shared 1.68
If useithreads is not defined, stop here. Do not try to repair a system Perl by replacing files under /usr/bin; install or select a correctly built Perl through your normal package or version-management process.
2. Create a worker that returns a value
Save this as /tmp/perl-thread-worker.pl. The worker receives a number, does ordinary private work, and returns a list. The main thread keeps the thread object so it can call join:
use strict;
use warnings;
use threads;
sub worker {
my ($name, $value) = @_;
return ($name, $value * $value);
}
my $first = threads->create(\&worker, 'first', 7);
my $second = threads->create(\&worker, 'second', 9);
my @first_result = $first->join();
my @second_result = $second->join();
print "$first_result[0] = $first_result[1]\n";
print "$second_result[0] = $second_result[1]\n";
Run it as an ordinary user:
$ perl /tmp/perl-thread-worker.pl
first = 49
second = 81
Checkpoint: both lines appear and the command exits successfully. The order in which workers execute is not a scheduling interface, but the output order here is stable because the main thread joins first before second. join waits, collects the return list, and performs the operating-system cleanup for that thread.
3. Make shared state explicit
Now replace the file with a version that has both workers add to one shared total. A shared variable protects Perl's internal data structure, but it does not make a multi-step read, modify and write operation indivisible. Put the lock around the complete update:
use strict;
use warnings;
use threads;
use threads::shared;
my $total :shared = 0;
sub add_value {
my ($value) = @_;
{
lock($total);
$total += $value;
}
}
my @threads = map {
threads->create(\&add_value, $_)
} (7, 9);
$_->join() for @threads;
print "total = $total\n";
Run it repeatedly:
$ perl /tmp/perl-thread-worker.pl
total = 16
$ perl /tmp/perl-thread-worker.pl
total = 16
The lock is released automatically when its enclosing block ends. There is no separate unlock call. Keep the locked section short and acquire multiple locks in one consistent order if you use them; opposite lock orders can deadlock.
Do not assume that $total++ is safe merely because $total is shared. The tutorial explicitly warns that even compound updates such as += and ++ need synchronisation. A shared array or hash also restricts what may be assigned to it: references must point to shared variables.
4. Choose join, detach or a queue deliberately
Use join for work whose result or completion matters. It gives you a natural error boundary in the main thread and ensures cleanup. Use detach only for work that has no result you need and that may finish independently:
my $thread = threads->create(\&background_task);
$thread->detach();
A detached thread cannot later be joined, and any return data is lost. Do not detach a worker just to avoid deciding how its failure should be handled.
For a boss and several workers, pass jobs through Thread::Queue rather than building ad hoc shared arrays. An empty queue makes dequeue wait, and a sentinel such as undef can tell a worker to stop:
use Thread::Queue;
my $jobs = Thread::Queue->new();
$jobs->enqueue('job-001', 'job-002');
$jobs->enqueue(undef);
Check that the module is available before committing to that design:
$ perl -MThread::Queue -e 'print "Thread::Queue is available\n"'
Thread::Queue is available
5. Avoid the process-exit trap
When the main thread exits, Perl can terminate the process while other threads are still running. The tutorial describes this as an active-thread warning and failure. Falling off the end of the script is not a substitute for joining workers. Keep every joinable worker in a collection and join it before the main thread finishes.
This is also why the first example joins both thread objects. If a worker can fail, arrange a return value or exception-handling policy and still ensure that the main thread reaches a defined cleanup path. A fatal die or explicit exit ends the process and therefore all its threads.
6. Keep platform-sensitive behaviour out of the design
threads->yield() is only a scheduling hint. On many operating systems it does nothing, so it must not be used to make a protocol reliable. Use a queue, a lock, a semaphore or a condition mechanism when a worker must wait for another worker.
Perl data is thread-private by default, but process-wide state is not a private thread setting. Calls such as chdir, umask, identity changes and chroot affect the process. Mixing threads with fork or signals is platform-dependent and needs a separate design review. Also treat modules and external libraries as unsafe for concurrent use unless their documentation or testing establishes otherwise.
Done means
perl -V:useithreadsreportsdefine.- A worker was created with
threads->createand its return value was collected withjoin. - Shared state was marked with
:sharedand its compound update was protected bylock. - You know that
detachdiscards the join and that queues are safer for job hand-off than an unprotected shared array. - The main thread joins its workers before exiting, and no design depends on
yieldor thread execution order.