A high-performance, lock-free multi-producer single-consumer (MPSC) channel implementation in Zig, designed for low-latency concurrent messaging.
- Lock-free MPSC: Per-producer tail pointers eliminate contention
- Zero-copy batch API: Reserve/commit for efficient bulk operations
- Futex-based blocking: Energy-efficient waiting with exponential backoff
- Dynamic producers: Safe registration and retirement with termination guarantees
- SPSC/SPMC support: Unified API for different producer/consumer patterns
- Production-ready: Comprehensive tests and benchmarks
Add to your build.zig.zon:
.{
.name = "your-project",
.version = "0.1.0",
.dependencies = .{
.bchan = .{
.url = "https://github.com/boonzy00/bchan/archive/refs/tags/v0.2.0.tar.gz",
.hash = "1220xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", // Run `zig fetch --save <url>` to get the actual hash
},
},
}Then in build.zig:
const bchan = b.addModule("bchan", .{
.source_file = .{ .path = "deps/bchan/src/lib.zig" },
});const std = @import("std");
const bchan = @import("bchan");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Create MPSC channel with 4 max producers
const ch = try bchan.Channel(u64).init(allocator, 1024, .MPSC, 4);
defer ch.deinit();
// Register a producer
const producer = try ch.registerProducer();
defer ch.unregisterProducer(producer);
// Send messages
try std.testing.expect(producer.trySend(42));
try std.testing.expect(producer.trySend(1337));
// Receive messages
const val1 = ch.tryReceive();
const val2 = ch.tryReceive();
std.debug.print("Received: {} {}\n", .{val1.?, val2.?});
}// Reserve space for batch
var ptrs: [64]?*u64 = undefined;
const reserved = producer.reserveBatch(&ptrs);
if (reserved > 0) {
// Fill the batch
for (0..reserved) |i| {
if (ptrs[i]) |ptr| {
ptr.* = i;
}
}
// Commit atomically
producer.commitBatch(reserved);
}
// Receive batch
var buf: [64]u64 = undefined;
const received = ch.tryReceiveBatch(&buf);// Blocking send
producer.send(42);
// Blocking receive
const val = ch.receive();See examples/simple.zig for a basic usage example, and benches/ for complete benchmark examples.
benches/batch.zig- High-throughput MPSC with batchingbenches/spsc.zig- SPSC performance testbenches/mpsc_vyukov.zig- Vyukov MPMC comparison
Run benchmarks with:
# Build optimized binaries
zig build -Doptimize=ReleaseFast
# Run MPSC benchmark 5 times with stats
./scripts/run_bench_mpsc.sh
# Run Vyukov comparison
./scripts/run_vyukov.sh| Scenario | Producers | Throughput | Notes |
|---|---|---|---|
| MPSC (batched, zero-copy) | 16 | 968 M msg/s | Mean of 5 runs |
| MPSC (batched, zero-copy) | 4 | 798 M msg/s | Mean of 5 runs |
| SPSC (batched, zero-copy) | 1 | 150 M msg/s | Mean of 7 runs, σ ≈ 10 M msg/s, governor-locked |
| SPSC (single-message) | 1 | 8–15 M msg/s | For comparison |
| Vyukov MPMC (4p4c) | 4 | 19 M msg/s | Reference implementation |
Performance Comparison
bchan MPSC (16p, batched) ██████████████████████████████ 968 M/s
bchan SPSC (batched, 1p1c) ████████████████████ 150 M/s
bchan SPSC (single-msg) ███████ 8–15 M/s
Vyukov MPMC ██ 19 M/s
All figures measured with CPU governor locked to performance and cores 0/1 isolated via cset shield.
Reproducibility note: Batched SPSC throughput measured via scripts/run_peak_locked.sh (batch=64, 35 s duration, 7 repetitions). Individual run values ranged from 128–156 M msg/s; mean 150 M msg/s, σ ≈ 10 M msg/s. See scripts/run_peak_locked.sh (SPSC) and scripts/run_scaling_locked.sh (batch sweep) for one-click reproducible commands.
pub fn init(allocator: std.mem.Allocator, capacity: u64, mode: Mode, max_producers: u32) !Channel(T)
pub fn deinit(self: *Channel(T)) voidpub fn registerProducer(self: *Channel(T)) !ProducerHandle
pub fn unregisterProducer(self: *Channel(T), handle: ProducerHandle) voidpub fn trySend(self: ProducerHandle, item: T) bool
pub fn send(self: ProducerHandle, item: T) void
pub fn trySendBatch(self: ProducerHandle, items: []const T) usize
pub fn sendBatch(self: ProducerHandle, items: []const T) usizepub fn reserveBatch(self: ProducerHandle, ptrs: []?*T) usize
pub fn commitBatch(self: ProducerHandle, count: usize) voidpub fn tryReceive(self: *Channel(T)) ?T
pub fn receive(self: *Channel(T)) T
pub fn tryReceiveBatch(self: *Channel(T), buffer: []T) usize
pub fn receiveBatch(self: *Channel(T), buffer: []T) usizeSee bchan_algorithm.md for detailed algorithm documentation.
- API Reference: Complete API documentation with examples
- Performance Guide: Optimization techniques and benchmarking
- Development Guide: Contributing and development workflows
- Algorithm Details: Technical implementation details
Run the full test suite:
zig build testIncludes correctness tests for all modes, edge cases, and concurrent scenarios.
Contributions welcome! Please see CONTRIBUTING.md for guidelines.
MIT License - see LICENSE file.
See CHANGELOG.md for version history.
- crossbeam-channel - Rust channels
- moodycamel::ConcurrentQueue - C++ lock-free queues
- zig-gamedev - Zig game development ecosystem
- Experiment with backoff strategies, padding/alignment, and futex thresholds to tune performance.
- Add CI jobs to run tests and (optionally) quick benches on supported runners. Add CI jobs to run tests and (optionally) quick benches on supported runners.