Perl's threads.pm implements "heavy" threads, where each thread is a complete clone of the interpreter. While this model provides excellent isolation, it creates unique challenges when working with FFI. If you aren't careful, you can end up with clobbered memory or race conditions in the JIT engine.
This chapter teaches you how to safely manage Affix in a multithreaded environment by respecting the boundary between setup and execution.
The Recipe
We will build a simple worker pool where multiple Perl threads call into a single C mathematical function.
use v5.40;
use threads;
use Affix qw[:all];
use Affix::Build;
# PHASE 1: Initialization (Main Thread)
# All setup MUST happen before threads are spawned.
my $c = Affix::Build->new();
$c->add( \<<~'END', lang => 'c' );
#include <math.h>
double heavy_calc(double input) {
// Simulate a CPU-heavy mathematical task
return sin(input) * cos(input) + sqrt(input);
}
END
my $lib = $c->link;
# Register the type and bind the function
affix $lib, 'heavy_calc', [Double] => Double;
# PHASE 2: Execution (Worker Threads)
sub worker ($id) {
say "Worker $id: Starting on OS thread " . threads->tid();
# Each thread can safely call the bound function
for ( 1 .. 100_000 ) {
my $res = heavy_calc( rand() );
}
return "Worker $id: Done";
}
# Spawn 4 worker threads
my @threads = map { threads->create( \&worker, $_ ) } 1 .. 4;
# Gather results
say $_->join() for @threads;
say 'All workers finished safely.';
How It Works
The Initialization Rule
The most important rule in Affix thread safety is: Initialize in the main thread.
Functions that modify Affix's internal global state are not thread-safe. This includes:
load_library/locate_libaffix/wrap/direct_affixtypedef
When threads->create is called, Perl clones the entire interpreter. Affix ensures that the trampolines and the registered types are available in the child threads. However, if you attempt to call affix inside a child thread, you risk corrupting the internal JIT registry.
Thread-Safe Trampolines
Once a function is bound (via affix or wrap), the resulting machine code is read-only and thread-safe. Multiple Perl threads can jump into the same C function simultaneously without locking.
Thread Context (THX)
When a C function is called, Affix handles the perl context (the aTHX macro in XS terms). Each thread has its own context and Affix ensures that when a C function returns, the data is marshalled back into the correct thread's stack.
Modern Callbacks and Foreign Threads
If a C library spawns its own native OS threads (not ithreads) and tries to trigger an Affix callback, Affix performs a context injection in an attempt to keep things flowing. It detects that the thread does not have a Perl interpreter attached and attempts to provide a temporary, safe environment for the Perl subroutine to run. This is what allows Affix to work with GUI event loops and audio drivers.
Kitchen Reminders
- C Library Safety: Just because Affix is thread-safe doesn't mean the library you are wrapping is. If the C function uses static variables or non-atomic global state, you must use
Thread::Semaphorein Perl to gate access. - Shared Memory: Pointers created with
mallocare just memory addresses. You can pass them between threads, but Perl'sthreads.pmdoes not automatically synchronize access to that memory. Usememcpyormemsetcarefully! - Memory Leaks: If you allocate memory in a thread using
malloc, ensure it is either freed in that thread or the ownership is clearly defined. Perl's garbage collector only cleans up managed pins when their specific thread-local scalar goes out of scope.