Acme-Parataxis

 view release on metacpan or  search on metacpan

Changes.md  view on Meta::CPAN

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [v0.0.10] - 2026-02-22

This version comes with a dynamic thread pool and an improved API.

### Added
- New ergonomic API using exported functions like `async { ... }`, `fiber { ... }`, and `await( $target )`.

### Changed

LICENSE  view on Meta::CPAN

modifying or distributing the Package, you accept this license. Do not
use, modify, or distribute the Package, if you do not accept this
license.

(11)  If your Modified Version has been derived from a Modified
Version made by someone other than you, you are nevertheless required
to ensure that your Modified Version complies with the requirements of
this license.

(12)  This license does not grant you the right to use any trademark,
service mark, tradename, or logo of the Copyright Holder.

(13)  This license includes the non-exclusive, worldwide,
free-of-charge patent license to make, have made, use, offer to sell,
sell, import and otherwise transfer the Package with respect to any
patent claims licensable by the Copyright Holder that are necessarily
infringed by the Package. If you institute patent litigation
(including a cross-claim or counterclaim) against any party alleging
that the Package constitutes direct or contributory patent
infringement, then this Artistic License to you shall terminate on the
date that such litigation is filed.

README.md  view on Meta::CPAN

fibers; it simply prevents the scheduler from starting new ones.

# THREAD POOL CONFIGURATION

`Acme::Parataxis` uses a native thread pool to handle blocking tasks. While it manages itself automatically, you can
tune its behavior using these functions.

## `set_max_threads( $count )`

Sets the maximum number of worker threads the pool is allowed to spawn. By default, this is set to the number of
logical CPU cores detected on your system (up to a hard limit of 64).

```
# Limit the pool to 4 threads
set_max_threads(4);
```

## `max_threads( )`

Returns the currently configured maximum thread pool size.

README.md  view on Meta::CPAN


## 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.

lib/Acme/Parataxis.c  view on Meta::CPAN

#ifdef _WIN32
    return GetCurrentProcessorNumber();
#elif defined(__linux__)
    return sched_getcpu();
#else
    return -1;
#endif
}

/**
 * @brief Detects the number of logical cores available on the system.
 *
 * @return int CPU count (minimum 1).
 */
int get_cpu_count() {
#ifdef _WIN32
    SYSTEM_INFO sysinfo;
    GetSystemInfo(&sysinfo);
    int count = sysinfo.dwNumberOfProcessors;
    return (count > 0) ? count : 1;
#elif defined(__APPLE__) || defined(__FreeBSD__)

lib/Acme/Parataxis.pod  view on Meta::CPAN

fibers; it simply prevents the scheduler from starting new ones.

=head1 THREAD POOL CONFIGURATION

C<Acme::Parataxis> uses a native thread pool to handle blocking tasks. While it manages itself automatically, you can
tune its behavior using these functions.

=head2 C<set_max_threads( $count )>

Sets the maximum number of worker threads the pool is allowed to spawn. By default, this is set to the number of
logical CPU cores detected on your system (up to a hard limit of 64).

    # Limit the pool to 4 threads
    set_max_threads(4);

=head2 C<max_threads( )>

Returns the currently configured maximum thread pool size.

=head1 BLOCKING & I/O FUNCTIONS

lib/Acme/Parataxis.pod  view on Meta::CPAN


=head2 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.

=head2 The 'Final Transfer' Requirement

In a symmetric coroutine model (using C<transfer( )>), fibers don't have a natural 'parent' to return to. I've added
fallback logic to return to the C<last_sender> or the main thread on exit but it's good practice to explicitly
C<transfer( )> back to a partner fiber or the C<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.

=head2 C<is_done( )> vs. Destruction

A fiber being C<is_done( )> simply means its Perl code has finished executing. The underlying C-level memory (stacks,
context, etc.) is not immediately freed until the C<Acme::Parataxis> object is destroyed or the runtime performs its
final C<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.

t/005_symmetric.t  view on Meta::CPAN

use v5.40;
use Test2::V1 -ipP;
use blib;
use Acme::Parataxis;
$|++;
#
subtest 'Direct transfer dance' => sub {
    my ( $p1, $p2 );
    my $log = '';
    $p1 = Acme::Parataxis->new(
        code => sub {
            $log .= '1';
            diag 'Fiber 1: Transferring to 2...';
            $p2->transfer();
            $log .= '3';
            diag 'Fiber 1: Transferring to 2 again...';
            $p2->transfer();
            $log .= '5';
            diag 'Fiber 1: Returning...';
            return 'Final 1';
        }
    );
    $p2 = Acme::Parataxis->new(
        code => sub {
            $log .= '2';
            diag 'Fiber 2: Transferring back to 1...';
            $p1->transfer();
            $log .= '4';
            diag 'Fiber 2: Transferring back to 1 again...';
            $p1->transfer();

            # In a symmetric model, we should transfer back to exit cleanly if we want someone to continue
            diag 'Fiber 2: Final transfer back to 1...';
            $p1->transfer();
        }
    );
    diag 'Starting the dance...';
    $p1->call();

    # Ensure both are recognized as done
    $p1->is_done;
    $p2->is_done;
    is $log, '12345', 'Control flowed symmetrically between fibers';
    ok $p1->is_done, 'Fiber 1 finished';
    ok $p2->is_done, 'Fiber 2 finished';
};
subtest 'Transfer with arguments' => sub {
    my ( $p1, $p2 );
    my @received;
    $p1 = Acme::Parataxis->new(
        code => sub {
            my $val = Acme::Parataxis->yield();
            push @received, $val;

t/008_preemption.t  view on Meta::CPAN

use v5.40;
use Test2::V1 -ipP;
use blib;
use Acme::Parataxis;
$|++;
#
my $log = '';

# Set threshold to 5
Acme::Parataxis::set_preempt_threshold(5);
my $c1 = Acme::Parataxis->new(
    code => sub {
        for ( 1 .. 10 ) {
            $log .= 'A';
            Acme::Parataxis->maybe_yield();
        }
    }
);
my $c2 = Acme::Parataxis->new(
    code => sub {
        for ( 1 .. 10 ) {
            $log .= 'B';
            Acme::Parataxis->maybe_yield();
        }
    }
);

# Call C1. It should run 5 times and then maybe_yield will switch back to main
# because the threshold is hit.
# WAIT: who is the parent? Main.
diag 'Calling C1...';
my $res1 = $c1->call();
is $log, 'AAAAA', 'C1 yielded after 5 iterations';
diag 'Calling C2...';
my $res2 = $c2->call();
is $log, 'AAAAABBBBB', 'C2 yielded after 5 iterations';
diag 'Resuming C1...';
$c1->call();
is $log, 'AAAAABBBBBAAAAA', 'C1 finished its remaining iterations';
diag 'Resuming C2...';
$c2->call();
is $log, 'AAAAABBBBBAAAAABBBBB', 'C2 finished its remaining iterations';
#
done_testing();



( run in 0.728 second using v1.01-cache-2.11-cpan-5735350b133 )