Acme-Parataxis
view release on metacpan or search on metacpan
These functions **suspend** the current fiber and offload the actual blocking work to the native thread pool.
## `await_sleep( $ms )`
Suspends the fiber for `$ms` milliseconds. While the background thread sleeps, other fibers can continue to execute.
## `await_read( $fh, $timeout = 5000 )`
Suspends the fiber until the filehandle `$fh` is ready for reading, or the `$timeout` (in milliseconds) is reached.
```perl
my $status = Acme::Parataxis->await_read($socket);
if ($status > 0) {
my $data = <$socket>;
}
```
## `await_write( $fh, $timeout = 5000 )`
Suspends the fiber until the filehandle `$fh` is ready for writing.
## `await_core_id( )`
Offloads a request to the thread pool and returns the ID of the CPU core that handled the job.
# MANUAL FIBER MANAGEMENT
Advanced users can manage context switching themselves without using the integrated scheduler.
## `new( code => $sub )`
Instantiates a new fiber. The `code` argument must be a subroutine reference.
```perl
my $fiber = Acme::Parataxis->new(code => sub {
my $arg = Acme::Parataxis->yield("Initial data");
return "Done with $arg";
});
```
## `call( @args )`
Explicitly switches control to the fiber and passes `@args`. Arguments can be scalars, hash/array references, or
objects. This establishes a parent/child relationship: when the fiber yields or completes, control returns to the
caller.
## `transfer( @args )`
A "symmetric" switch. Suspends the current context and moves directly to the target fiber. No parent/child relationship
is established. Like `call`, it supports passing arbitrary Perl data via `@args`.
# PREEMPTION
## `maybe_yield( )`
Increments an internal operation counter for the current fiber. If the counter reaches the threshold set by
`set_preempt_threshold`, the fiber automatically yields.
```perl
while (my $row = $sth->fetch) {
process($row);
Acme::Parataxis->maybe_yield( ); # Cooperatively prevent starvation
}
```
## `set_preempt_threshold( $val )`
Sets the number of `maybe_yield` increments before a forced yield occurs. Default is 0 (preemption disabled).
# Class Methods
## `tid( )`
Returns the unique OS Thread ID of the main interpreter thread.
## `current_fid( )`
Returns the unique numeric ID of the currently executing fiber, or -1 if called from the "root" (main) context.
## `root( )`
Returns a proxy object representing the initial execution context. This is useful for `transfer( )`ing control back to
the main thread from a symmetric coroutine.
# Acme::Parataxis OBJECT METHODS
## `fid( )`
Returns the unique numeric ID of the fiber object.
## `is_done( )`
Returns true if the fiber has finished execution (either by returning or dying). Once a fiber is done, its internal ID
is released and it can no longer be called.
# Acme::Parataxis::Future OBJECT METHODS
## `await( )`
Suspends the current fiber until the future is ready. Returns the result or **dies** if the task encountered an error.
## `is_ready( )`
Returns true if the task associated with the future has completed.
## `result( )`
Returns the task result immediately. Croaks if the future is not yet ready.
# INTEGRATING SYNCHRONOUS MODULES
To use synchronous modules (like `HTTP::Tiny`) in a non-blocking way, you can subclass their handle or transport
methods and use a `while` loop combined with `yield('WAITING')`. This ensures the fiber yields control until the
underlying I/O is ready.
```perl
# Example: A cooperative HTTP::Tiny subclass
{
package My::HTTP;
use parent 'HTTP::Tiny';
sub _open_handle {
}
$c->transfer('DONE');
});
$c = Acme::Parataxis->new(code => sub {
my $item = Acme::Parataxis->yield( ); # Initial wait
while (1) {
last if $item eq 'DONE';
say "Consumer: Eating $item";
$item = $p->transfer( );
}
});
$c->call( ); # Prime consumer
$p->call( ); # Start producer
```
# BEST PRACTICES & GOTCHAS
- **Avoid blocking syscalls:** Never call blocking `sleep( )` or `sysread( )` on the main interpretation thread.
Always use the `await_*` equivalents to offload work to the pool.
- **Thread Safety:** While Perl code remains single-threaded, background tasks run on separate OS threads. Shared
C-level data (if accessed via FFI) must be mutex-protected.
- **Stack Limits:** Each fiber is allocated a 512KB stack by default. This is more than sufficient for most
Perl code and allows for high concurrency with a small memory footprint. Extremely deep recursion or massive regex
backtracking might still hit limits.
- **Efficiency:** The native thread pool is initialized dynamically upon the first asynchronous request. It
starts with a small "seed" pool and grows on demand up to the configured limit. Worker threads use condition
variables to sleep efficiently when idle, ensuring near-zero CPU usage when no background tasks are pending.
- **Reference Cycles:** Be careful when passing fiber objects into their own closures, as this can create
memory leaks.
# GORY TECHNICAL DETAILS
## Architectural Inspiration
The concurrency model in Parataxis is heavily inspired by the **Wren** programming language, specifically its treatment
of fibers as the primary unit of execution and its deterministic cooperative scheduling.
## Stack Virtualization
On Unix-like systems, we use `ucontext.h` to manage stack and register state. On Windows, we leverage the native
`Fiber API`. In both cases, we perform heart surgery on the Perl interpreter by manually teleporting its internal
global pointers (the `PL_*` variables) between contexts.
## Shared CVs and Pad Virtualization
A significant challenge in Perl green threads is the shared nature of PadLists and the global `CvDEPTH` counter. In
debug builds of Perl, calling a shared subroutine from multiple fibers can trigger internal assertions (like
`AvFILLp(av) == -1`). Parataxis includes a specialized workaround that surgically cleans the next landing pad before
every context switch to satisfy these assertions without clobbering active lexical state.
## `eval` vs. `try/catch`
While `feature 'try'` is available in modern Perl, manually teleporting interpreter state can occasionally confuse the
compiler's expectations for stack unwinding. Standard `eval { ... }` remains the most predictable way to handle
exceptions within fibers.
## Signal Handling
Signals are delivered to the main process thread. Perl handles these at 'safe points,' which in this module typically
occur during a context switch (yield, transfer, or call). If you send a signal while a fiber is suspended, it will
generally be processed when the fiber is resumed and hits the next internal Perl opcode.
## The 'Final Transfer' Requirement
In a symmetric coroutine model (using `transfer( )`), fibers don't have a natural 'parent' to return to. I've added
fallback logic to return to the `last_sender` or the main thread on exit but it's good practice to explicitly
`transfer( )` back to a partner fiber or the `root( )` context to ensure your application logic remains predictable.
Leaving a fiber to just 'fall off the end' is like walking out of a room without closing the door; eventually, the
draft will bother someone.
## `is_done( )` vs. Destruction
A fiber being `is_done( )` simply means its Perl code has finished executing. The underlying C-level memory (stacks,
context, etc.) is not immediately freed until the `Acme::Parataxis` object is destroyed or the runtime performs its
final `cleanup( )`. This is why you might see memory usage stay flat even after a fiber finishes, until the garbage
collector finally catches up with the object.
# AUTHOR
Sanko Robinson <sanko@cpan.org>
# 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.
( run in 2.345 seconds using v1.01-cache-2.11-cpan-995e09ba956 )