Skip to content

Commit 458d659

Browse files
committed
Document event loop pool feature
1 parent a8f3cbf commit 458d659

File tree

2 files changed

+98
-0
lines changed

2 files changed

+98
-0
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@
4343

4444
### Added
4545

46+
- **Event Loop Pool** - Pool of event loops for parallel Python coroutine execution
47+
- `py_event_loop_pool:get_loop/0` - Get event loop for current process (process affinity)
48+
- `py_event_loop_pool:create_task/3,4` - Submit async task to pool
49+
- `py_event_loop_pool:run/3,4` - Blocking call via pool
50+
- `py_event_loop_pool:spawn_task/3,4` - Fire-and-forget task
51+
- `py_event_loop_pool:await/1,2` - Wait for task result
52+
- Process affinity ensures same PID always routes to same loop (ordered execution)
53+
- Uses `persistent_term` for O(1) loop access
54+
- Configurable via `{event_loop_pool_size, N}` (default: schedulers count)
55+
- Benchmarks: 417k tasks/sec (fire-and-collect), 164k tasks/sec (50 concurrent processes)
56+
4657
- **ByteChannel API** - Raw byte streaming channel without term serialization
4758
- `py_byte_channel:new/0,1` - Create byte channel (with optional backpressure)
4859
- `py_byte_channel:send/2` - Send raw bytes to Python

docs/asyncio.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,6 +1407,93 @@ The async task API is fully thread-safe:
14071407

14081408
This means you can safely call `py_event_loop:create_task` from within a callback that's already running on a dirty NIF scheduler.
14091409

1410+
## Event Loop Pool
1411+
1412+
The `py_event_loop_pool` module provides a pool of event loops for parallel Python coroutine execution. Inspired by libuv's "one loop per thread" model, each loop has its own worker and maintains event ordering.
1413+
1414+
### Process Affinity
1415+
1416+
All tasks from the same Erlang process are routed to the same event loop (via PID hash). This guarantees that timers and related async operations from a single process execute in order.
1417+
1418+
```
1419+
┌─► [loop_1] ──► [worker_1] ──► ordered execution
1420+
[process] ──► [hash(PID)] ─┼─► [loop_2] ──► [worker_2] ──► ordered execution
1421+
└─► [loop_N] ──► [worker_N] ──► ordered execution
1422+
```
1423+
1424+
### API
1425+
1426+
The pool provides the same API as `py_event_loop`, but with automatic load distribution:
1427+
1428+
```erlang
1429+
%% Get event loop for current process (always the same loop for same PID)
1430+
{ok, LoopRef} = py_event_loop_pool:get_loop().
1431+
1432+
%% Submit task and await result
1433+
Ref = py_event_loop_pool:create_task(math, sqrt, [16.0]),
1434+
{ok, 4.0} = py_event_loop_pool:await(Ref).
1435+
1436+
%% Blocking call
1437+
{ok, 4.0} = py_event_loop_pool:run(math, sqrt, [16.0]).
1438+
1439+
%% Fire-and-forget
1440+
ok = py_event_loop_pool:spawn_task(logger, info, [<<"message">>]).
1441+
1442+
%% Pool statistics
1443+
#{num_loops := N, supported := true} = py_event_loop_pool:get_stats().
1444+
```
1445+
1446+
### Configuration
1447+
1448+
Configure the pool size via application environment:
1449+
1450+
```erlang
1451+
%% sys.config
1452+
[
1453+
{erlang_python, [
1454+
%% Number of event loops (default: erlang:system_info(schedulers))
1455+
{event_loop_pool_size, 8}
1456+
]}
1457+
].
1458+
```
1459+
1460+
### When to Use
1461+
1462+
| Use Case | Module |
1463+
|----------|--------|
1464+
| Single caller, ordered tasks | `py_event_loop` |
1465+
| Multiple callers, parallel execution | `py_event_loop_pool` |
1466+
| High throughput, many concurrent processes | `py_event_loop_pool` |
1467+
1468+
### Performance
1469+
1470+
Benchmarks on 14-core system with Python 3.14:
1471+
1472+
| Pattern | Throughput |
1473+
|---------|------------|
1474+
| Sequential (single loop) | 83k tasks/sec |
1475+
| Sequential (pool) | 150k tasks/sec |
1476+
| Concurrent (50 processes) | 164k tasks/sec |
1477+
| Fire-and-collect (10k tasks) | 417k tasks/sec |
1478+
1479+
### Example: Parallel Processing
1480+
1481+
```erlang
1482+
%% Process items in parallel using multiple Erlang processes
1483+
%% Each process gets its own event loop for ordered execution
1484+
process_batch(Items) ->
1485+
Parent = self(),
1486+
Pids = [spawn_link(fun() ->
1487+
Results = [begin
1488+
Ref = py_event_loop_pool:create_task(processor, handle, [Item]),
1489+
py_event_loop_pool:await(Ref)
1490+
end || Item <- Chunk],
1491+
Parent ! {done, self(), Results}
1492+
end) || Chunk <- partition(Items, 100)],
1493+
1494+
[receive {done, Pid, R} -> R end || Pid <- Pids].
1495+
```
1496+
14101497
## See Also
14111498

14121499
- [Reactor](reactor.md) - Low-level FD-based protocol handling

0 commit comments

Comments
 (0)