Cozi
Concurrency primitives for Zig
Install / Use
/learn @zxubian/CoziREADME
cozi - concurrency primitives for Zig
Fibers, thread pools, futures - all in userland Zig. Oh My!
Goals
- empower Zig users to solve concurrent & parallel engineering problems
- at a higher level of expressiveness than basic synchronization primitives (mutex/condvar)
- while maintaining the low-level control that is crucial for systems programming
- to that end, provide a toolbox of software components with the following properties:
- orthogonal - components are meaningful and useful by themselves. Components have minimal coupling between themselves.
- composable - components can be combined to produce more powerful behaviours.
- extensible - we cannot anticipate every use-case. So, our APIs must be designed in a way that allows users of the library to integrate their custom solutions.
Installation
Minimum Supported Zig Version
0.15.0-dev.1262+e12dc4947
Steps
- Install package:
zig fetch --save git+https://github.com/zxubian/cozi.git#main
- Add
cozimodule to your executable:
// build.zig
const cozi = b.dependency("cozi", .{});
exe.root_module.addImport("cozi", cozi.module("root"));
- Import and use:
Build Configuration Options
You can modify the behavior of cozi by overriding build options at import timing.
- For convenience, copy the build scipt into your repository
- Register the build options, and forward the results to
coziwhen registering it as a dependency:
//build.zig
const exe_mod = b.createModule(.{...});
// import cozi's build script
const cozi_build = @import("./cozi_build.zig");
// register cozi's build options & gather results
const cozi_build_options = cozi_build.parseBuildOptions(b);
// pass the parsed results to the cozi dependency
const cozi = b.dependency("cozi", cozi_build_options);
exe_mod.addImport("cozi", cozi.module("root"));
- You can then check the list of available build options:
zig build -h
# > -Dcozi_log=[bool] Enable verbose logging from inside the cozi library
# ...
For each option that is not overridden, cozi will use the default defined in BuildOptions.
Stability Guarantees
cozi is experimental and unstable. Expect main branch to occasionally break.
Examples
# get list of available examples
zig build example-run
# build & run specific example
zig build example-run -Dexample-name="some_example"
Docs
# build documentation
zig build docs
# host docs on local http server
python3 -m http.server 8000 -d ./zig-out/docs
# open in browser
http://localhost:8000/index.html
Features & Roadmap
Executors & Schedulers
Executoris a type-erased interface representing an abstract task queue:- users can submit Runnables (an abstract representation of a task) for eventual execution
cozi's concurrency primitives (Futures,Fibers) can run on anyExecutorExecutoris to asynchronous task execution what Allocator is to memory management.
const executor = thread_pool.executor();
executor.submit(some_function, .{args}, allocator);
// eventually, some_function(args) will be called.
// exact timing depends on the specific Executor implementation
Fibers - stackfull cooperatively-scheduled user-space threads
- threads: like OS threads, fibers represents a "thread" of execution, i.e. an independent sequence of instructions together with its execution context (stack space)
- stackful: user must allocate memory for each fiber's execution stack (in contrast to e.g. stackless coroutines)
- cooperatively-scheduled: fibers are not pre-empted by the system or the
coziruntime- Instead, each fiber itself is responsible for releasing control of the underlying thread and allow other fibers to run
- When in this state, the fiber is refered to as being suspended or parked.
Comparison to other languages
- Fibers are an example of Green Threads
- Fibers are similar to goroutines in GoLang, and coroutines in Kotlin
Supported Platforms
See Coroutine - Supported Platforms
const Ctx = struct {
sum: usize,
wait_group: std.Thread.WaitGroup = .{},
// non-blocking mutex for fibers
mutex: Fiber.Mutex = .{},
pub fn run(
self: *@This(),
) void {
for (0..10) |_| {
{
// Fibers running on thread pool may access
// shared variable `sum` in parallel.
// Fiber.Mutex provides mutual exclusion without
// blocking underlying thread.
self.mutex.lock();
defer self.mutex.unlock();
self.sum += 1;
}
// Suspend execution here (allowing for other fibers to be run),
// and immediately reschedule self with the Executor.
Fiber.yield();
}
self.wait_group.finish();
}
};
var ctx: Ctx = .{ .sum = 0 };
const fiber_count = 4;
ctx.wait_group.startMany(fiber_count);
// Run 4 fibers on 2 threads
for (0..4) |fiber_id| {
try Fiber.goWithNameFmt(
Ctx.run,
.{&ctx},
allocator,
executor,
"Fiber #{}",
.{fiber_id},
);
}
// Synchronize Fibers running in a thread pool
// with the launching (main) thread.
ctx.wait_group.wait();
Futures & Promises
[!NOTE]
documentation WIP
Stackfull Coroutine - a function you can suspend & resume
Coroutine - Supported Platforms
See issue.
| Arch\OS | MacOS | Windows | Linux | |:-------:|:-----:|:-------:|:-----:| | aarch64 | ✅ | ❌ | ✅ | | x86_64 | ❌ | ✅ | ❌ |
const cozi = @import("cozi");
const Coroutine = cozi.Coroutine;
// ...
const Ctx = struct {
pub fn run(ctx: *Coroutine) void {
log.debug("step 1", .{});
ctx.@"suspend"();
log.debug("step 2", .{});
ctx.@"suspend"();
log.debug("step 3", .{});
}
};
var coro: Coroutine.Managed = undefined;
try coro.initInPlace(Ctx.run, .{&coro.coroutine}, gpa.allocator());
defer coro.deinit();
for (0..3) |_| {
coro.@"resume"();
}
assert(coro.isCompleted());
Long-term initiatives
integration with Zig async/await
- It is currently unclear what direction Zig will go with for language support of async/await.
- Once the Zig Language direction is decided, we will consider the best way to integrate it with the library.
Memory Management Policy:
- API must allow fine-grained control over allocations for users who care
- it's nice to provide "managed" API for users who don't (e.g. for testing)
- regardless of memory management approach chosen by the user, library must minimize number of runtime allocations
Acknowledgements
The design of cozi is heavily based on prior work, especially the concurrency course taught by Roman Lipovsky at MIPT. The author would like to express his deepest gratitude to Roman for all of the knowledge that he shares publicly, and for his dedication to education in technology. This library began as a fun exercise to go along with the course, and would not exist without it.
Honourable mentions:
- YACLib
- GoLang
- Rust
