As you likely know, Perl 5 is fundamentally single threaded. While threads.pm allows for concurrency, it creates "heavy" threads that clone the entire interpreter state, leading to slow startup times and high memory consumption. For high-concurrency systems, the preferred primitive is the fiber or green threads.

In this chapter, we dissect Acme::Parataxis, a fiber system built with Affix that implements true fibers by performing low-level surgery on the Perl interpreter's internal state. My early goal for this was to demonstrate messing around with perl's internals from an affix'd shared library with the help of Affix::Build.

The Recipe

Interfacing with a fiber system requires binding to custom C code that understands the Perl C API.

use v5.40;
use Acme::Parataxis qw[:all];
$|++;
async {
    say 'Main task started';

    # 'fiber' is a shorter alias for 'spawn'
    my $f1 = fiber {
        say '  Task 1: Sleeping...';
        await_sleep(1000);
        return 42;
    };
    my $f2 = fiber {
        say '  Task 2: Performing I/O...';

        # ...
        return 'I/O Done';
    };

    # 'await' works on fibers and futures
    say 'Result 1: ' . await($f1);
    say 'Result 2: ' . await($f2);
};

How It Works

Acme::Parataxis is perhaps the most extreme example of what is possible with Affix. It doesn't just wrap a library; it redefines the execution flow of the Perl interpreter itself.

1. The Dual-Layer Switch

A fiber switch in Parataxis is a two-stage process performed in a custom C library bound via Affix.

Stage A: The OS Context Switch Affix binds to OS-specific primitives like SwitchToFiber (Windows) or swapcontext (Unix). This saves the CPU registers, including the Instruction Pointer and the Hardware Stack Pointer.

Stage B: The Perl Context Switch The C core defines a para_fiber_t struct that holds the entire state of a Perl interpreter thread. Acme::Parataxis uses Affix to bind a function that manually swaps over 20 global pointers found in the Perl core (PL_stack_sp, PL_markstack, PL_scopestack, etc.).

2. Architectural Inspiration: The Wren Model

The design of Acme::Parataxis is heavily inspired by the Wren scripting language. In Wren, fibers are the primary unit of execution and are designed to be...

Real-World Efficiency: Non-blocking HTTP

The true power of fibers is making asynchronous I/O look like simple synchronous code. By subclassing HTTP::Tiny and replacing its internal socket checks with await_read() and await_write(), we can perform multiple requests in parallel on a single thread:

async {
    my $http = My::CooperativeHTTP->new();
    my @urls = qw[http://perl.org http://metacpan.org];

    # Spawn concurrent requests
    my @futures = map { spawn { $http->get($_) } } @urls;

    # Wait for all to finish
    my @results = map { await $_ } @futures;
};

A functioning example using HTTP::Tiny in such a system comes bundled with the Acme::Parataxis dist.

Benchmarks: The Speed of Cooperation

Context switching in Acme::Parataxis happens at the nanosecond scale because the "trampoline" generated by Affix is optimized for your specific CPU architecture.

Benchmark: The Stress of 1,000 Fibers

This test measures the scheduler's ability to handle many short-lived fibers performing rapid yield/resume cycles.

use v5.40;
use Acme::Parataxis qw[:all];
use Time::HiRes     qw[time];
no warnings 'recursion';
async {
    my $total_fibers = 1000;
    my $iterations   = 5;
    say "Starting Stress Test: $total_fibers fibers, $iterations iterations...";
    my $start_time = time();
    for my $iter ( 1 .. $iterations ) {
        my $iter_start = time();
        my @futures;

        # Spawn a batch of 1,000 fibers
        for my $i ( 1 .. $total_fibers ) {
            push @futures, fiber {

                # Perform a tiny bit of CPU work
                my $val = 0;
                $val += $_ for 1 .. 100;

                # Cooperatively yield control halfway through
                yield() if $i % 2 == 0;
                return $val;
            };
        }

        # Await the completion of the entire batch
        await $_ for @futures;
        my $elapsed = time() - $iter_start;
        say sprintf '  Iteration %2d completed in %.4fs', $iter, $elapsed;
    }
    my $total_elapsed = time() - $start_time;
    say sprintf 'Total time: %.4fs', $total_elapsed;
    say 'Average time per fiber lifecycle: ' . ( $total_elapsed / ( $total_fibers * $iterations ) ) . 's';
};

In terms of throughput, fibers can outperform threads.pm for many tasks because they avoid the kernel overhead of OS-level scheduling.

Real-World Demo: Threaded I/O Offloading

The following demonstration shows how Acme::Parataxis handles blocking operations (like sleep or read) by offloading them to a native C thread pool while the main Perl thread continues to schedule other fibers.

use v5.40;
use Acme::Parataxis qw[:all];
use Time::HiRes     qw[time];
async {
    say 'Main: Initial thread pool size is ' . Acme::Parataxis::get_thread_pool_size();
    my @futures;
    for my $id ( 1 .. 5 ) {
        push @futures, fiber {
            my $sleep_time = int( rand(1000) ) + 500;
            say "  [Fiber $id] Will sleep for ${sleep_time}ms in background pool...";
            my $start = time();

            # This suspends the fiber, the C pool sleeps, and we are resumed when done.
            await_sleep($sleep_time);
            my $elapsed = int( ( time() - $start ) * 1000 );
            say "  [Fiber $id] Woke up after ${elapsed}ms!";
            return "Result $id";
        };
    }
    say 'Main: All fibers spawned. Total pool size: ' . Acme::Parataxis::get_thread_pool_size();

    # Wait for all workers to finish
    my @results = map { await $_ } @futures;
    say 'Main: All fibers finished!';
};

Kitchen Reminders