NAME
Acme::Parataxis::Blocking - Run CPU-bound Perl closures on background interpreters (threads.pm) or forked children
VERSION
Version 0.001_001
SYNOPSIS
use Acme::Parataxis;
use Acme::Parataxis::Blocking qw[spawn_blocking];
my $f = Acme::Parataxis::Blocking->spawn_blocking( sub { heavy_parse($blob) } );
my $parsed = $f->await; # your fibers kept running while $blob was parsed
On a perl that cannot do CPU offload with threads, the same promise is available from a forked child instead. It is a separate entry point rather than a fallback, so the choice is visible at the call site:
use Acme::Parataxis;
use Acme::Parataxis::Blocking qw[spawn_blocking_fork];
my $f = spawn_blocking_fork( sub { heavy_parse($blob) }, $blob );
my $parsed = $f->await;
DESCRIPTION
Runs CPU-bound Perl work on a dedicated background Perl interpreter (a real OS thread cloned with threads->create) and returns an Acme::Parataxis::Future carrying its result, so a second of JSON/image-code work never stalls your cooperative fibers.
This is the one part of the Acme::Parataxis project that uses threads.pm, and it keeps that machinery lazy: threads, threads::shared, and Thread::Queue are only required inside the first spawn_blocking call, so loading any other part of this distribution never loads them and stays free of any threads associations: Acme::Parataxis is about cooperatively scheduling fibers in a single interpreter, and perl's native threads are only a means to get a second, isolated interpreter for work that the C pool cannot run.
spawn_blocking requires a Perl built with ithreads (useithreads); on a non-threaded perl it croaks with a clear message, and loading this library never touches threads (they are only required on the first call), so non-threaded perls keep using everything else unchanged. It must be called from inside a scheduled fiber and from a real-time run: it croaks under run( virtual >=> 1 ), because real OS work cannot be fast-forwarded by the mock clock.
spawn_blocking_fork is the same promise for a perl that cannot clone interpreters. It runs the closure in a forked child process, so a non-ithreads perl still gets CPU offload, and it never requires threads either.
FUNCTIONS
spawn_blocking( $code, @args )
Also callable as a plain function: spawn_blocking( sub { ... }, @args ). The args arrive in the closure's @_, and the result (or die), is marshalled back through threads::shared; unshareable results (a CODE ref, a resource) become the Future's error instead of crashing the caller. The future composes with await, with_timeout, with_cancel, and wait_all exactly like any other.
Hard limits, like the C pool's: each call clones a fresh interpreter, so the closure sees only a copy-in copy-out snapshot - plain/structured values cross intact, in-place mutations never leak back, and the closure must not call back into Acme::Parataxis scheduler objects or blocking APIs while it runs. At most set_max_blocking_threads() interpreters run at once (default 4); a call past the cap parks the calling fiber until a slot frees. Cancelling the await does not stop the closure (a real OS thread cannot be yanked); it withdraws the waiter, and the slot releases when the closure actually finishes.
set_max_blocking_threads( $count )
Sets the concurrency cap for spawn_blocking background interpreters (like set_max_threads for the C pool). The default is 4, or whatever the PARATAXIS_SB_THREADS environment variable holds, and this call must happen before the first spawn_blocking - once the pool is in use its capacity is live and changing it croaks. Returns the new cap.
set_max_blocking_threads(8); # up to 8 background interpreters at once
max_blocking_threads()
Returns the configured spawn_blocking capacity.
spawn_blocking_fork( $code, @args )
Runs $code in a forked child process and returns an Acme::Parataxis::Future carrying its result. The args arrive in the closure's @_, and the result (or die) is marshalled back over a pipe with Storable; unshareable results (a CODE ref, a resource) become the Future's error instead of crashing the caller. The future composes with await, with_timeout, with_cancel, and wait_all exactly like any other.
#!/usr/bin/env perl
use v5.40;
use Acme::Parataxis qw[run fiber await_sleep];
use Acme::Parataxis::Blocking qw[spawn_blocking_fork];
sub collatz_steps {
my ($n) = @_;
my $steps = 0;
while ( $n != 1 ) { $n = $n % 2 ? 3 * $n + 1 : $n / 2; $steps++ }
return $steps;
}
my $report = run(
sub {
my $f = spawn_blocking_fork( sub { collatz_steps(27) } );
fiber { await_sleep(20); print "your fibers kept running meanwhile\n" };
return $f->await;
}
);
print "27 takes $report steps\n";
This is a separate entry point rather than a fallback inside spawn_blocking, on purpose. The two have different honest limits - a child is a process and a cloned interpreter is not - and a caller should be able to see which one it asked for at the call site instead of inheriting a silent per-build difference. If spawn_blocking quietly became spawn_blocking_fork on a perl without ithreads, every copy-in copy-out detail, every error message, and every cancellation behaviour in a program would change under it with no edit and no diagnostic.
The copy-in copy-out rules match spawn_blocking's, and the reasons differ. spawn_blocking clones a fresh interpreter; this forks a copy of yours, so the closure sees the caller's world as it stood at the fork - its args, and whatever its captured lexicals held then. Either way the only thing that travels back is the frozen result, and in-place mutations in the child never come back to you.
The differences that are real:
No
threadsanywhere.threads,threads::sharedandThread::Queueare never loaded, so this works on a perl built without ithreads and leaves the property that loading this distribution acquires no threads association intact.Cancellation can actually stop the work. Cancelling the
awaitbehaves exactly as it does forspawn_blocking- it withdraws the waiter and changes nothing else - but a child is a process, sokill_blocking_forkcan signal it and end the work for real. See below.The child inherits the parent's open descriptors, including any attached event loop's sockets. A closure that reads or writes through one of those would act on a copy of the descriptor rather than the original, and an inherited listening socket keeps its port reserved until the child exits. Keep closures away from inherited descriptors.
The closure must not call back into Acme::Parataxis. The child's scheduler state is a copy and means nothing there.
At most set_max_blocking_forks() children run at once (default 4); a call past the cap parks the calling fiber until a slot frees. A separate cap from the thread one, because a forked child and a cloned interpreter are not the same resource and coupling them would let one starve the other silently.
A harvester fiber is always scheduled, whether or not anyone awaits the future, so the pipe is drained and the child reaped even if you never look at the result - no zombies, and no deadlock when the payload is larger than the pipe buffer, because the child simply blocks in write until the harvester's next chunk.
Like spawn_blocking this is wall-clock only and croaks under run( virtual >=> 1 ). It also croaks unless the platform has a real fork(2): on MSWin32, where perl's fork is a threads.pm emulation, and on any build whose d_fork is not define.
kill_blocking_fork( $future, $signal )
Signals a running spawn_blocking_fork child - TERM unless told otherwise - and returns true if a signal was delivered, or false if the child was already gone. It takes the future rather than a pid, because the future is what the caller holds and a pid it would have to be told separately is a pid it can get wrong.
This is the one thing the thread pool cannot offer. A spawn_blocking closure is a real OS thread and cannot be yanked; a child can be signalled, so runaway work genuinely stops instead of running to completion off the side of the program. The future then resolves with an error, never with a partial result.
#!/usr/bin/env perl
use v5.40;
use Acme::Parataxis qw[run fiber await_sleep];
use Acme::Parataxis::Blocking qw[spawn_blocking_fork kill_blocking_fork];
my $finished = run(
sub {
my $f = spawn_blocking_fork( sub { my $n = 0; $n++ while 1 } );
fiber { await_sleep(200); kill_blocking_fork($f) };
my $result = eval { $f->await };
return $@ ? "stopped: $@" : 'finished';
}
);
print "$finished\n";
set_max_blocking_forks( $count )
Sets the concurrency cap for spawn_blocking_fork children. The default is 4, or whatever the PARATAXIS_SBF_FORKS environment variable holds, and this call must happen before the first spawn_blocking_fork - once the pool is in use its capacity is live and changing it croaks. Returns the new cap.
set_max_blocking_forks(8); # up to 8 forked children at once
max_blocking_forks()
Returns the configured spawn_blocking_fork capacity.
PLATFORM WARNING
threads' interpreter clone (perl_clone) access-violates on some 5.42.x builds (notably Strawberry MSWin32) once certain things are loaded, crashing the whole process at the first threads->create. Two separate triggers are known. (1) A feature 'class' file that declares a method DESTROY on a second class block in the same file - the in-tree guard classes (Semaphore::Guard, and the Acme::Parataxis::Sync mutex/rwlock guards) are therefore classic blessed packages, not perlclasses. (2) Independently and content-independently, merely loading particular namespaces has the same crash - observed here for the whole Acme::Parataxis::Sync family (Sync, Sync<I::Mutex> etc.), Acme::Parataxis::TVar, and Acme::Parataxis::Driver. On an affected perl there is no in-library workaround (the module's contents and even the guard classes are irrelevant to trigger (2)); if you load any of those namespaces and then intend to use spawn_blocking, test on a short-lived process first, or arrange for the closures to run before such modules load.
A second Windows-only behavior is a stall, not a crash. While any socket wait is in flight anywhere in the process - a fiber parked in await_read or await_write, or another thread blocked in a raw recv - the perl_clone behind the first threads->create blocks until that wait returns: the clone duplicates every open descriptor (PerlIO_fdupopen), and Windows holds the duplication while a socket has an outstanding operation. The stall tracks the wait's remaining time, not a fixed budget (measured: 4969 ms for a 5000 ms parked read; 493 ms when only 478 ms remained). It is identical with select() and with WSAPoll, so no Parataxis mechanism is involved - it is the known Windows behavior described at https://perlmonks.org/?node_id=1078634 - and Linux and the BSDs are unaffected. If the added latency matters, arrange spawn_blocking calls before any fiber starts reading, or keep socket waits out of the picture during a spawn.
spawn_blocking_fork is not affected by any of the above. It never calls threads->create and so never reaches perl_clone, which removes both the access violation and the Windows stall; in exchange it inherits a forked child's limitations, which are described under spawn_blocking_fork above. It is, of course, still unavailable on MSWin32.
SEE ALSO
Acme::Parataxis, Acme::Parataxis::Future, Acme::Parataxis::Semaphore, threads, Thread::Queue =head1 AUTHOR
Sanko Robinson https://github.com/sanko
LICENSE
Copyright (C) Sanko Robinson.
This library is free software; you can redistribute it and/or modify it under the terms found in the Artistic License 2.