NAME

Acme::Parataxis::Blocking - Run CPU-Bound Perl Closures on Background Interpreters or Forked Children

SYNOPSIS

use v5.40;
use Acme::Parataxis qw[async fiber await_sleep];
use Acme::Parataxis::Blocking qw[spawn_blocking spawn_blocking_fork kill_blocking_fork];

async {
    # Offload CPU-heavy computation to a background thread interpreter:
    my $future = spawn_blocking( sub ($data) {
        return heavy_cpu_digest($data);
    }, $payload );

    # Other fibers continue running smoothly while the background thread computes
    fiber {
        await_sleep(10);
        say 'Cooperative fibers are still running!';
    };

    my $result = $future->await;
    say "Digest: $result";
};

DESCRIPTION

The Acme::Parataxis native C worker pool handles blocking I/O and sleeps, but it cannot run arbitrary Perl code without blocking the main interpreter. Acme::Parataxis::Blocking offloads heavy, CPU-bound Perl closures to separate execution contexts so your cooperative fibers never drop frames or stall.

Two backends are provided:

  • spawn_blocking: Runs the closure on an isolated background Perl interpreter using native threads (threads->create). Requires a Perl built with ithreads (useithreads). Thread dependencies (threads, threads::shared, Thread::Queue) are loaded lazily on the first invocation.

  • spawn_blocking_fork: Runs the closure in a forked child process, marshaling arguments and results over a pipe using Storable. Works on non-threaded Perls (POSIX platforms where d_fork is defined).

Both functions return an Acme::Parataxis::Future that can be awaited, combined with wait_all/wait_any, or bounded with with_timeout.

FUNCTIONS

spawn_blocking( $code, [@args] )

my $future = spawn_blocking( sub ($x, $y) {
    return $x ** $y;
}, 2, 64 );

Executes $code on an isolated background Perl interpreter. @args are passed to the closure.

  • Copy-in / Copy-out: The closure receives a snapshot of its arguments and captured lexicals. Memory is not shared with the parent interpreter; in-place mutations in the worker do not affect the caller.

  • Return values: The result (or exception) is serialized back via threads::shared::shared_clone. Plain scalars, arrayrefs, and hashrefs cross safely. Non-serializable structures (such as CODE refs or open handles) cause the future to fail with an error.

  • No scheduler access: The worker closure runs in an isolated interpreter and must not invoke Acme::Parataxis scheduling primitives.

Concurrence is capped by max_blocking_threads(). Calls exceeding capacity park the calling fiber until a slot frees. Must be called within a scheduled fiber and under a real-time scheduler (croaks under virtual => 1).

set_max_blocking_threads( $count )

set_max_blocking_threads(8);

Sets the maximum number of concurrent background interpreter threads (default 4, or overridden by the PARATAXIS_SB_THREADS environment variable). Must be called before the first spawn_blocking invocation.

max_blocking_threads( )

Returns the current thread concurrency limit.

spawn_blocking_fork( $code, [@args] )

my $future = spawn_blocking_fork( sub ($n) {
    return compute_pi_digits($n);
}, 10_000 );

Forks a child process to run $code. Arguments and results are marshaled over an internal pipe using Storable.

Unlike thread workers, forked workers can be cancelled and terminated midway through execution via kill_blocking_fork().

Not available on Windows (where Perl's fork() is an emulation over threads) or platforms without fork(2). Croaks under run( virtual => 1 ).

kill_blocking_fork( $future, [$signal = 'TERM'] )

kill_blocking_fork( $future, 'KILL' );

Delivers $signal to the child process associated with $future. Returns true if the signal was delivered, or false if the child was already reaped. The future resolves with an error indicating termination.

set_max_blocking_forks( $count )

set_max_blocking_forks(8);

Sets the maximum number of concurrent forked child processes (default 4, or overridden by PARATAXIS_SBF_FORKS). Must be called before the first spawn_blocking_fork invocation.

max_blocking_forks( )

Returns the current fork concurrency limit.

Recipes

Offloading long-running calculations

use Acme::Parataxis qw[async await];
use Acme::Parataxis::Blocking qw[spawn_blocking];

async {
    my $task = spawn_blocking( sub {
        my $sum = 0;
        $sum += $_ for 1 .. 100_000_000;
        return $sum;
    } );

    say 'Calculating in background...';
    my $total = await($task);
    say "Total: $total";
};

Cancelling runaway background work

Native threads cannot be forcibly killed, but forked processes can:

use Acme::Parataxis qw[async fiber await_sleep];
use Acme::Parataxis::Blocking qw[spawn_blocking_fork kill_blocking_fork];

async {
    my $f = spawn_blocking_fork( sub { 1 while 1 } );

    fiber {
        await_sleep(500);
        say 'Work took too long; killing child...';
        kill_blocking_fork($f);
    };

    my $err = eval { $f->await } ? undef : $@;
    say "Stopped with error: $err";
};

PLATFORM WARNING

  • Strawberry Perl / Win32 ithreads crashes

    On certain Perl 5.42.x builds on Windows, calling threads->create triggers a perl_clone segmentation fault if classes with a method DESTROY inside a feature 'class' block have been compiled. In addition, loading specific namespaces (including Acme::Parataxis::Sync or Acme::Parataxis::TVar) can trigger this crash. If targeting Windows with spawn_blocking, run compute tasks before loading sync modules, or use non-class fallback guards.

  • Windows socket cloning stall

    Under Windows, while any fiber is parked in await_read or await_write on a socket, invoking threads->create stalls until the socket wait returns. This occurs because Windows holds descriptor duplication during outstanding socket operations. Schedule spawn_blocking calls before initiating socket loops to avoid latency spikes.

spawn_blocking_fork does not use threads->create and is unaffected by these issues (though it is not available on Windows).

SEE ALSO

Acme::Parataxis, Acme::Parataxis::Future, threads, Storable

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.