Coroutines & Async Networking

PXX features a built-in, cooperative, single-threaded coroutine scheduler and an asynchronous networking reactor. This allows a single operating system thread to manage thousands of concurrent connections and tasks without the overhead of OS threads.


Cooperative Scheduling — the scheduler unit

The scheduler unit provides the core coroutine lifestyle routines. Coroutines are cooperatively scheduled, meaning a running task continues execution until it voluntarily yields control back to the scheduler.

Key Routines


Asynchronous Sockets — the asyncnet unit

The asyncnet unit provides non-blocking TCP socket operations integrated with the scheduler's reactor. When a socket operation would block (returning EAGAIN), the coroutine automatically parks on the reactor and yields, waking up only when the socket becomes ready.

Key Routines


Under the Hood

The coroutine system is built on two key pillars:

  1. Low-level context switching: A compiler intrinsic (__pxxcoswitch) saves and restores CPU registers and stack pointers.
  2. Procedural types: Coroutines are defined using plain Pascal procedures, requiring no target-specific assembly entry shims.

On x86-64 Linux, the scheduler utilizes a high-performance epoll reactor. Socket wait states (WaitReadable / WaitWritable) and sleep states are registered as epoll events. On other targets (or platforms where epoll is unavailable), these operations gracefully degrade to a busy-poll CoYield loop, maintaining API compatibility across the entire PXX target matrix.


Compiling Example

The following program implements a concurrent TCP echo server and client running on a single thread. The client sends a message, and the server receives and echoes it back.

This program compiles and runs on the pinned compiler:

program async_echo_demo;

uses scheduler, asyncnet, sysutils;

const
  PORT = 28888;
  BUF_SIZE = 256;

procedure ServerCo(arg: Pointer);
var
  lfd, cfd: Integer;
  buf: array[0..BUF_SIZE-1] of Char;
  bytes: Int64;
  s: AnsiString;
  i: Integer;
begin
  lfd := TcpListen(PORT);
  if lfd < 0 then
  begin
    writeln('Server failed to listen');
    Exit;
  end;
  
  writeln('Server listening on port ', PORT);
  cfd := TcpAccept(lfd);
  if cfd < 0 then
  begin
    writeln('Server failed to accept');
    TcpClose(lfd);
    Exit;
  end;
  
  writeln('Server accepted connection');
  
  // Read client message (yields coroutine if data is not yet ready)
  bytes := TcpRecv(cfd, @buf[0], BUF_SIZE - 1);
  if bytes > 0 then
  begin
    // Copy character array to string for printing
    SetLength(s, bytes);
    for i := 0 to bytes - 1 do
      s[i + 1] := buf[i];
    writeln('Server received: ', s);
    
    // Echo the bytes back to the client
    TcpSend(cfd, @buf[0], bytes);
  end;
  
  TcpClose(cfd);
  TcpClose(lfd);
  writeln('Server shut down');
end;

procedure ClientCo(arg: Pointer);
var
  cfd: Integer;
  msg: AnsiString;
  buf: array[0..BUF_SIZE-1] of Char;
  bytes: Int64;
  s: AnsiString;
  i: Integer;
begin
  // Give the server a moment to start up
  CoSleep(100);
  
  writeln('Client connecting...');
  cfd := TcpConnect(PORT);
  if cfd < 0 then
  begin
    writeln('Client failed to connect');
    Exit;
  end;
  
  writeln('Client connected');
  msg := 'Hello from PXX Async!';
  
  // Send message over the socket
  TcpSend(cfd, @msg[1], Length(msg));
  
  // Read the echoed message back
  bytes := TcpRecv(cfd, @buf[0], BUF_SIZE - 1);
  if bytes > 0 then
  begin
    SetLength(s, bytes);
    for i := 0 to bytes - 1 do
      s[i + 1] := buf[i];
    writeln('Client received echo: ', s);
  end;
  
  TcpClose(cfd);
  writeln('Client shut down');
end;

begin
  // Spawn both coroutines on the single thread
  Spawn(@ServerCo, nil);
  Spawn(@ClientCo, nil);
  
  // Hand control to the scheduler reactor
  RunUntilDone;
  writeln('All done');
end.

Output

Server listening on port 28888
Client connecting...
Server accepted connection
Client connected
Server received: Hello from PXX Async!
Server shut down
Client received echo: Hello from PXX Async!
Client shut down
All done

For real OS threads (TThread) and data-parallel loops (parallel for) that use every core — the other axis of concurrency — see Threads & parallelism.

Next