# 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`](../../p5/core/perlfunc/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::shared` fails at compile time — the module is not shipped. - `:shared` is parsed but has no underlying interpreter to share to. - [`lock`](../../p5/core/perlfunc/lock) is a silent no-op, as documented on its reference page. - `Thread::Queue` and `Thread::Semaphore` are 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](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: ```perl 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`: ```perl 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: ```perl 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`](../../p5/core/perlfunc/lock) places an advisory mutex on a shared datum for the rest of the enclosing block: ```perl 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.** `lock` blocks other callers of `lock` on the same datum. A thread that reads or writes `$total` without calling `lock` is not serialised. - **Block-scoped.** Release happens at block exit, not at the end of the `lock` statement. There is no `unlock`. - **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 &sub` are all valid. Under pperl, `lock` accepts its argument and returns — no mutex, no blocking. See the [`lock`](../../p5/core/perlfunc/lock) reference page for the full behaviour and the *Differences from upstream* note. ## Races The canonical race: ```perl 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: ```perl { 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](parallel), section *How reduction detection works*. ## Deadlocks Two threads, two locks, acquired in opposite order: ```perl 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 `$x` before `$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: ```perl 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`](../../p5/core/perlfunc/map) or [`grep`](../../p5/core/perlfunc/grep) built-ins — see [Alternatives](alternatives). ## Semaphores `Thread::Semaphore` is a counter with blocking `down` and `up`: ```perl 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`](../../p5/core/perlfunc/chdir) — current working directory - [`chroot`](../../p5/core/perlfunc/chroot) — root directory, unrecoverable - [`umask`](../../p5/core/perlfunc/umask) — default file mode mask - `setuid` / `setgid` — effective user / group - Signal 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`](../../p5/core/perlfunc/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](parallel) for the details and [Alternatives](alternatives) for the pperl-native patterns. ## See also - [`lock`](../../p5/core/perlfunc/lock) — reference page for the only shared-data primitive with a pperl compile-time parse - [ithreads basics](ithreads-basics) — spawning, joining, detaching - [Alternatives](alternatives) — the pperl-native concurrency patterns that replace the primitives above - [Parallel Execution](parallel) — how auto-parallelisation handles reductions and detects side effects - [`fork`](../../p5/core/perlfunc/fork) — process-level concurrency with no shared memory