Shared data#
Under upstream Perl 5.42, sharing data between ithreads is explicit:
variables are private by default, and become visible across threads
only when the :shared attribute is applied or a value is passed
through a thread-safe primitive. This chapter walks through the
full upstream shared-data surface — threads::shared,
lock, semaphores, queues, condition
variables — and flags pperl’s status at each section.
pperl status at a glance#
None of the sharing primitives on this page do anything at runtime under pperl:
use threads::sharedfails at compile time — the module is not shipped.:sharedis parsed but has no underlying interpreter to share to.lockis a silent no-op, as documented on its reference page.Thread::QueueandThread::Semaphoreare not available.
Reading this page is still worth your time if you maintain upstream Perl code or need to read existing ithreaded programs. For new concurrency on pperl, skip to Alternatives.
Shared and unshared variables#
By default, every Perl variable in an ithread is private. When you spawn a thread, the child starts with a copy of the parent’s data and goes its own way:
use threads;
my $private = 1;
threads->create(sub { $private++ })->join;
print $private, "\n"; # 1 — parent untouched
To cross the thread boundary, mark the variable :shared and
use threads::shared:
use threads;
use threads::shared;
my $shared :shared = 1;
threads->create(sub { $shared++ })->join;
print $shared, "\n"; # 2 — write survived into parent
For an aggregate, every element becomes shared automatically:
my @queue :shared;
my %counts :shared;
Only simple scalars and references to other shared variables may be stored into a shared aggregate. Storing a reference to an unshared variable dies — upstream refuses the assignment to protect the sharing boundary.
The lock built-in#
lock places an advisory mutex on a
shared datum for the rest of the enclosing block:
use threads;
use threads::shared;
my $total :shared = 0;
sub calc {
while (my $result = compute_chunk()) {
lock $total; # block until mutex available
$total += $result;
} # mutex released here
}
Properties worth remembering:
Advisory only.
lockblocks other callers oflockon the same datum. A thread that reads or writes$totalwithout callinglockis not serialised.Block-scoped. Release happens at block exit, not at the end of the
lockstatement. There is nounlock.Recursive per thread. The same thread may re-acquire a lock it already holds; other threads still block.
Works on aggregates.
lock @queue,lock %h,lock &subare all valid.
Under pperl, lock accepts its argument and returns — no mutex, no
blocking. See the lock reference
page for the full behaviour and the Differences from upstream
note.
Races#
The canonical race:
use threads;
use threads::shared;
my $x :shared = 1;
my $t1 = threads->create(sub { my $foo = $x; $x = $foo + 1 });
my $t2 = threads->create(sub { my $bar = $x; $x = $bar + 1 });
$t1->join; $t2->join;
print $x, "\n"; # 2 or 3 — depends on scheduling
Two threads read $x, compute a new value, and write back. If both
reads land before either write, both writes store the same value
and one increment is lost. Even $x++ on a shared scalar is not
atomic.
The fix is to hold the lock across the read-modify-write:
{
lock $x;
$x++;
}
Under pperl this problem does not arise because auto-parallelisation does not expose shared mutable state to user code. The analyser declines to parallelise anything that writes a non-reduction global. See Parallel Execution, section How reduction detection works.
Deadlocks#
Two threads, two locks, acquired in opposite order:
my $x :shared = 4;
my $y :shared = 'foo';
threads->create(sub { lock $x; sleep 1; lock $y })->join;
threads->create(sub { lock $y; sleep 1; lock $x })->join;
# Both threads wait forever.
The standard defences:
Fixed lock order. Every thread that needs multiple locks acquires them in the same global order. Always
$xbefore$y.Short critical sections. Hold a lock only for the work that must be serialised, not for anything else.
Fewer locks. Protect the whole structure with one lock rather than one per field.
Queues — the preferred communication channel#
Thread::Queue is a thread-safe FIFO. Producer threads enqueue,
consumer threads dequeue, and the queue handles all synchronisation
internally:
use threads;
use Thread::Queue;
my $q = Thread::Queue->new;
my $worker = threads->create(sub {
while (defined(my $item = $q->dequeue)) {
handle($item);
}
});
$q->enqueue($_) for @tasks;
$q->enqueue(undef); # sentinel: tell worker to stop
$worker->join;
dequeue blocks when the queue is empty. Enqueueing undef (or
any agreed sentinel) is the conventional shutdown signal.
Queues replace most explicit lock / cond_wait patterns in modern
Perl threaded code. They are harder to get wrong.
Under pperl the equivalent shape is a straight Perl array combined
with the parallel map or
grep built-ins — see
Alternatives.
Semaphores#
Thread::Semaphore is a counter with blocking down and up:
use threads;
use Thread::Semaphore;
my $sem = Thread::Semaphore->new(4); # 4 concurrent I/O slots
sub do_io {
$sem->down;
# ... perform I/O; at most 4 threads here at once ...
$sem->up;
}
With a starting count of 1, a semaphore behaves like a mutex but
must be released explicitly — unlike lock, which is
scope-released. Starting counts above 1 model pools of identical
resources: a quota of concurrent open files, a number of allowed
simultaneous HTTP calls, and so on.
Condition variables#
cond_wait and cond_signal — paired with lock on a shared
scalar — let one thread wait for another to signal a state change.
They resemble POSIX pthread_cond_wait / pthread_cond_signal.
For the overwhelming majority of cases a queue does the same job
more simply; reach for condition variables only when a queue does
not fit the data shape.
Process-scope side effects#
Even with nothing shared at the Perl level, threads share the OS process. Anything that changes process state changes it for every thread:
chdir— current working directorychroot— root directory, unrecoverableumask— default file mode masksetuid/setgid— effective user / groupSignal dispositions — there is no per-thread signal mask in Perl
This is a classic source of “mysterious” cross-thread effects in otherwise shared-nothing code. Both upstream and pperl inherit this from the OS.
Differences from upstream#
pperl does not ship threads::shared, Thread::Queue, or
Thread::Semaphore. The :shared attribute, cond_wait, and
cond_signal are not recognised. lock
compiles but is a no-op; this matches upstream’s behaviour under a
build without thread support.
pperl exposes parallelism at a different layer — the JIT compiler dispatches eligible loop bodies across a Rayon thread pool, with automatic analysis of counter variables, reduction accumulators, and side effects. No user-visible synchronisation primitives are needed because the analyser refuses to parallelise any loop whose body is not trivially independent. See Parallel Execution for the details and Alternatives for the pperl-native patterns.
See also#
lock— reference page for the only shared-data primitive with a pperl compile-time parseithreads basics — spawning, joining, detaching
Alternatives — the pperl-native concurrency patterns that replace the primitives above
Parallel Execution — how auto-parallelisation handles reductions and detects side effects
fork— process-level concurrency with no shared memory