NAME
WiringPi::API::WORKERS - Concurrency & background-worker examples
DESCRIPTION
Worked, runnable examples for running background work concurrently with your main program using WiringPi::API. The worker() helper described here is implemented in WiringPi::API 3.18 and verified on Pi 5 hardware; the snippets run as written.
worker() is fork-based by default and needs no use threads and no threaded Perl - an ithread mechanism is a documented opt-in only. For reacting to GPIO edges in the background, see docs/interrupt-examples.md / WiringPi::API::INTERRUPTS (background_interrupt).
See the WiringPi::API POD ("CONCURRENCY / BACKGROUND WORKERS") for the authoritative per-function reference.
ABOUT THESE EXAMPLES
This document covers running work concurrently with the main program - distinct from reacting to interrupts.
Prefer
worker(). It hides the spawn mechanism, the loop and the lifecycle: your body carries nofork, nouse threads, nodetach, nowhile (1)and no cleanup. It is the general-purpose sibling ofbackground_interrupt.No threads required.
worker()forks by default and works on any Perl, threaded or not. The ithread mechanism (scenario 6) is an opt-in for users who specifically want shared-memory ergonomics on a threaded Perl.Pin numbering follows whichever setup you call:
setup()= wiringPi numbering,setup_gpio()= BCM. Examples usesetup().Mode constants for
pin_mode:INPUT=0,OUTPUT=1 (shown as integers).The setup-once-in-main contract: call
setup()/pin_modeonce, in the parent, before starting a worker. A fork worker inherits the configuration; you drive pins from inside the body.Sections 7-9 ("UNDER THE HOOD") show the raw
fork/threads->create/Async::Event::Intervalplumbing thatworker()packages up - read them to understand what happens beneath the helper, but most programs only needworker().
DECISION GUIDE
None of these need use threads except scenario 6. To hide the most plumbing, use worker() (scenarios 1-5).
fork vs thread in one line: the default fork worker is crash-isolated and works on any Perl but can't touch main's variables (hand data back with results/shared); a mechanism => 'thread' worker shares memory directly but needs a threaded Perl and pi_lock discipline. No shared-memory need? Use the default fork.
Run a background task and forget it (its own GPIO) - scenario 1 (
worker).Sample periodically; main reads the latest value - scenario 2 (
worker+interval/shared).Stream every value the worker produces back to main - scenario 3 (
worker+results).Do one background job once, then exit - scenario 4 (
worker+once).Several independent workers, each on its own pin - scenario 5 (
worker).Share memory directly between main and the worker - scenario 6 (
worker+mechanism => 'thread').React to a pin edge in the background -
background_interrupt(see WiringPi::API::INTERRUPTS).Understand/hand-roll the raw mechanism - scenarios 7, 8, 9.
BACKGROUND WORKERS (worker)
1. Heartbeat LED - a worker on its own pin
Why/when: Run a self-contained background task on its own GPIO while main does its own work; the simplest possible case.
Real-world: A status heartbeat LED blinking on its own cadence while the main program does its real work.
Main & background: The helper owns the loop and the lifecycle. You write only the body; worker() repeats it until you stop.
use strict;
use warnings;
use WiringPi::API qw(setup pin_mode write_pin worker);
setup();
pin_mode(2, 1); # OUTPUT, once in main
my $w = worker(sub {
write_pin(2, 1); sleep 1;
write_pin(2, 0); sleep 1;
});
# ... main does its own work ...
$w->stop; # idempotent; END reaps if you forget
No use threads, no fork, no detach, no while (1), no waitpid.
2. Periodic sampler handing data back (interval + shared)
Why/when: Timer-driven sampling where main only ever wants the latest reading, not every sample.
Real-world: Polling a sensor every second into a value the main app (a web handler or display loop) reads on demand.
Main & background: { interval => $secs } paces the loop (the body needs no sleep); { shared => 1 } publishes the body's return value as a lossy latest value the parent reads with $w->value.
use strict;
use warnings;
use WiringPi::API qw(setup pin_mode analog_read worker);
setup();
pin_mode(0, 0); # INPUT, once in main
my $w = worker(sub { analog_read(0) }, { interval => 1, shared => 1 });
while (1) {
my $latest = $w->value; # most recent sample, or undef until the first
print "latest: ", (defined $latest ? $latest : 'n/a'), "\n";
sleep 5;
}
$w->stop;
The channel is lossy - the worker never blocks on a slow reader, so value() gives you the most recent sample and discards the ones you didn't read.
3. Streaming every result (results)
Why/when: When you need every value the worker produces, in order, not just the latest.
Real-world: A logger that records each reading, or a counter feeding an event-loop via select.
Main & background: { results => 1 } length-frames every defined return value back over a pipe. Drain it with $w->read (non-blocking), or select on $w->fh.
use strict;
use warnings;
use WiringPi::API qw(setup pin_mode analog_read worker);
setup();
pin_mode(0, 0); # INPUT, once in main
my $w = worker(sub { analog_read(0) }, { interval => 0.5, results => 1 });
for (1 .. 20) {
while (defined(my $v = $w->read)) { # drain everything pending
print "sample: $v\n";
}
sleep 1;
}
$w->stop;
This is identical to background_interrupt's { results => 1 } channel.
4. A one-shot background task (once)
Why/when: A single background job - run it off the main path and let it exit on its own.
Real-world: Firing a one-shot solenoid pulse, or taking a single sensor reading, without blocking main.
Main & background: { once => 1 } runs the body exactly once; the child then exits and $w->running becomes false. You can still stop (idempotent) or just let the END reaper clean up.
use strict;
use warnings;
use WiringPi::API qw(setup pin_mode write_pin worker);
setup();
pin_mode(5, 1); # OUTPUT, once in main
my $w = worker(sub {
write_pin(5, 1);
select(undef, undef, undef, 0.2); # 200ms pulse
write_pin(5, 0);
}, { once => 1 });
# ... main carries on; the pulse fires in the background ...
$w->stop if $w->running; # usually already finished
5. Several workers on distinct pins
Why/when: Multiple independent background tasks at once, each owning its own pin.
Real-world: A multi-channel relay board where each channel toggles on its own cadence while main runs the control logic.
Main & background: Configure every pin once in main, then start one worker per pin. Each runs independently; each returns its own handle.
use strict;
use warnings;
use WiringPi::API qw(setup pin_mode write_pin worker);
setup();
pin_mode($_, 1) for (2, 3, 4); # OUTPUT - all config in main, up front
my @workers = (
worker(sub { write_pin(2, 1); sleep 1; write_pin(2, 0); sleep 1 }),
worker(sub { write_pin(3, 1); sleep 2; write_pin(3, 0); sleep 2 }),
worker(sub { write_pin(4, 1); sleep 3; write_pin(4, 0); sleep 3 }),
);
# ... main's own work ...
$_->stop for @workers;
Workers must drive distinct pins - see "THE SETUP-ONCE-IN-MAIN CONTRACT".
6. Shared memory - the opt-in ithread mechanism
Why/when: You specifically want to share memory directly between main and the worker (no IPC), and you have a threaded Perl.
Real-world: A counter or state machine the worker mutates and main reads in the same address space.
Main & background: { mechanism => 'thread' } runs the body in an ithread instead of a fork. It requires use threads (croaks otherwise) and rejects the results/shared pipe channels - share a :shared variable and serialize it with pi_lock/pi_unlock (keys 0-3) instead. stop sets the stop flag and joins the thread.
use strict;
use warnings;
use threads; # required for mechanism => 'thread'
use threads::shared;
use WiringPi::API qw(setup worker pi_lock pi_unlock);
setup();
my $count :shared = 0;
my $w = worker(sub {
pi_lock(0);
$count++;
pi_unlock(0);
select(undef, undef, undef, 0.1);
}, { mechanism => 'thread' });
while (1) {
pi_lock(0);
my $n = $count;
pi_unlock(0);
print "count: $n\n";
sleep 1;
}
$w->stop; # sets the stop flag and joins
Check for a threaded Perl with perl -V:useithreads (Raspberry Pi OS ships one).
REACTING TO INTERRUPTS IN THE BACKGROUND
worker() is for running background work, not for reacting to GPIO edges. To handle an edge in the background - fire a callback even while main is blocked - use background_interrupt, the interrupt-side sibling of worker(). It forks a child that arms the interrupt and runs your callback on each edge, and returns a handle with the same stop/pid/running shape:
use WiringPi::API qw(setup pin_mode background_interrupt INT_EDGE_RISING);
setup();
pin_mode(0, 0); # INPUT
my $h = background_interrupt(0, INT_EDGE_RISING, sub {
my ($edge, $ts_us) = @_;
# runs in the background on each rising edge
});
# ... main does its own work; the handler fires on its own ...
$h->stop;
The full interrupt story is in WiringPi::API::INTERRUPTS / docs/interrupt-examples.md.
THE SETUP-ONCE-IN-MAIN CONTRACT
The rule that keeps concurrent GPIO safe: do all configuration once, in main, before starting any worker; afterwards each context does only steady-state I/O on distinct pins.
Call
setup()/setup_gpio(), everypin_mode, and any device*Setuponce, in the parent, before the firstworker().A fork worker inherits that configuration; a thread worker shares it.
Afterwards, workers may freely
read_pin/write_pinon distinct pins. Never callsetup()/pin_mode/device*Setupconcurrently - they read-modify-write shared registers.For shared Perl data under
mechanism => 'thread', guard every access withpi_lock/pi_unlock(orthreads::shared'slock).
UNDER THE HOOD
These are the raw mechanisms worker() packages up. You rarely need them directly; they are here to show what the helper does and to cover cases it doesn't.
7. Manual fork
Why/when: The fork worker (scenario 1) without the helper - full control over the child, at the cost of writing the loop, the signal handling and the reaping yourself.
Main & background: The child is a separate process: truly concurrent and crash-isolated, but it cannot touch main's variables - pass data back via a pipe, and reap it yourself.
use strict;
use warnings;
use WiringPi::API qw(setup pin_mode write_pin);
setup();
pin_mode(2, 1); # OUTPUT - before fork
my $pid = fork // die "fork: $!";
if ($pid == 0) {
while (1) { # child: heartbeat forever
write_pin(2, 1); sleep 1;
write_pin(2, 0); sleep 1;
}
exit 0;
}
# ... parent's own work ...
kill 'TERM', $pid; # on shutdown
waitpid $pid, 0;
worker(sub {...}) is exactly this - the fork, the loop, the TERM handler and the waitpid - done for you, with an idempotent stop and an END-block reaper.
8. Raw ithreads (threads->create)
Why/when: The thread worker (scenario 6) without the helper - when you want to manage the thread object yourself.
Main & background: The body runs in its own interpreter; it can't see main's lexicals - share only via :shared variables guarded by pi_lock/lock.
use strict;
use warnings;
use threads;
use threads::shared;
use WiringPi::API qw(setup pin_mode read_pin pi_lock pi_unlock);
setup();
pin_mode(3, 0); # INPUT
my $latest :shared = 0;
my $thr = threads->create(sub {
while (1) {
my $v = read_pin(3);
pi_lock(0); $latest = $v; pi_unlock(0);
select(undef, undef, undef, 0.05);
}
});
# ... main reads $latest under pi_lock(0) ...
# To stop a hand-rolled thread you need your own shared flag + join;
# worker({mechanism=>'thread'}) provides exactly that.
$thr->detach;
worker(sub {...}, { mechanism = 'thread' })> wraps this with a shared stop flag and a clean stop/join, so you don't hand-roll the lifecycle.
9. Periodic work with Async::Event::Interval
Why/when: A fork-based CPAN module for periodic tasks with crash detection/restart and a shared scalar for the latest value. worker() with { interval, shared } (scenario 2) covers most of this without a dependency; reach for Async::Event::Interval when you specifically want its restart-on-crash.
Main & background: The callback runs in a forked child every interval; main reads the latest value via the module's shared_scalar (lossy).
use strict;
use warnings;
use Async::Event::Interval;
use WiringPi::API qw(setup pin_mode read_pin);
setup();
pin_mode(3, 0); # INPUT - before the event forks
my $event = Async::Event::Interval->new(1, \&sample); # forks; runs every 1s
my $latest = $event->shared_scalar;
$event->start;
while (1) {
print "latest: ", (defined $$latest ? $$latest : 'n/a'), "\n";
$event->restart if $event->error; # auto-recover a crashed sampler
sleep 2;
}
sub sample {
$$latest = read_pin(3);
}
Honest fit: this is a timer with latest-value (lossy) shared state - ideal for periodic sampling, wrong for edge interrupts (don't drop edges - keep those on the self-pipe, WiringPi::API::INTERRUPTS). Note Async::Event::Interval sets $SIG{CHLD} = 'IGNORE' and uses SysV shared memory at load time, so it does not compose with a hand-rolled fork/waitpid or with worker().
ANTI-PATTERNS TO AVOID
Reaching for
use threadsfirst.worker()is fork-based and needs no threaded Perl. Only usemechanism => 'thread'when you specifically want shared memory.Putting a loop inside the body.
worker()owns the loop - the body is one pass. Use{ interval => $secs }for pacing and{ once => 1 }for a single pass; awhile (1)inside the body defeatsstop/once/interval.Concurrent
pin_mode/setup/ device*Setup. Read-modify-write on shared registers; do them once, in main, before starting any worker. Onlyread_pin/write_pinon distinct pins are safe concurrently.Expecting a fork worker to see main's variables. Separate memory - hand data back with
{ results => 1 }/{ shared => 1 }, not a shared Perl variable.Touching
:shareddata without a lock (thread mode). Guard every access withpi_lock/pi_unlock(orlock). A bare$shared++from two threads races.Combining
mechanism => 'thread'withresults/shared. Those are fork pipe channels and are rejected under thread mode - share a:sharedvariable withpi_lockinstead.Mixing a hand-rolled
fork/waitpidwithAsync::Event::Interval. It sets$SIG{CHLD} = 'IGNORE'at load, which auto-reaps children - your ownwaitpidthen fails. Pick one process-management model.
API REFERENCE FOR THESE EXAMPLES
setup()/setup_gpio()-
Init (wiringPi / BCM numbering); once, in main. Returns int status (
0= ok). pin_mode($pin, $mode)-
0=INPUT,1=OUTPUT. write_pin($pin, $val)/read_pin($pin)-
Pin I/O;
read_pinreturns the pin level (0/1). worker(\&body [, \%opts])-
Run
\&bodyin the background.\%opts:{once, interval, results, shared, mechanism}. Returns a handle$wwithstop/pid/running/read/fh/value. pi_lock($key)/pi_unlock($key)-
Mutex (keys 0-3) for shared state under thread mode.
background_interrupt(...)-
Background edge handler (see WiringPi::API::INTERRUPTS). Returns a handle
$h.
SEE ALSO
WiringPi::API, WiringPi::API::INTERRUPTS
AUTHOR
Steve Bertrand, <steveb@cpan.org>