Files
Odin/core/nbio/doc.odin
2026-01-25 12:32:26 +00:00

214 lines
6.6 KiB
Odin
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
package nbio implements a non-blocking I/O and event loop abstraction layer
over several platform-specific asynchronous I/O APIs.
More examples can be found in Odin's examples repository
at [[ examples/nbio ; https://github.com/odin-lang/examples/tree/master/nbio ]].
**Event Loop**:
Each thread may have at most one event loop associated with it.
This is enforced by the package, as running multiple event loops on a single
thread does not make sense.
Event loops are reference counted and managed by the package.
`acquire_thread_event_loop` and `release_thread_event_loop` can be used
to acquire and release a reference. Acquiring must be done before any operation
is done.
The event loop progresses in ticks. A tick checks if any work is to be done,
and based on the given timeout may block waiting for work.
Ticks are typically done using the `tick`, `run`, and `run_until` procedures.
Example:
package main
import "core:nbio"
import "core:time"
import "core:fmt"
main :: proc() {
err := nbio.acquire_thread_event_loop()
assert(err == nil)
defer nbio.release_thread_event_loop()
nbio.timeout(time.Second, proc(_: ^nbio.Operation) {
fmt.println("Hellope after 1 second!")
})
err = nbio.run()
assert(err == nil)
}
**Time and timeouts**:
Timeouts are intentionally *slightly inaccurate* by design.
A timeout is not checked continuously, instead, it is evaluated only when
a tick occurs. This means if a tick took a long time, your timeout may be ready
for a bit of time already before the callback is called.
The function `now` returns the current time as perceived by the event
loop. This value is cached at least once per tick so it is fast to retrieve.
Most operations also take an optional timeout when executed.
If the timeout completes before the operation, the operation is cancelled and
called back with a `.Timeout` error.
**Threading**:
The package has a concept of I/O threads (threads that are ticking) and worker
threads (any other thread).
An I/O thread is mostly self contained, operations are executed on it, and
callbacks run on it.
If you try to execute an operation on a thread that has no running event loop
a panic will be executed. Instead a worker thread can execute operations onto
a running event loop by taking it's reference and executing operations with
that reference.
In this case:
- The operation is enqueued from the worker thread
- The I/O thread is optionally woken up from blocking for work with `wake_up`
- The next tick, the operation is executed by the I/O thread
- The callback is invoked on the I/O thread
Example:
package main
import "core:nbio"
import "core:net"
import "core:thread"
import "core:time"
Connection :: struct {
loop: ^nbio.Event_Loop,
socket: net.TCP_Socket,
}
main :: proc() {
workers: thread.Pool
thread.pool_init(&workers, context.allocator, 2)
thread.pool_start(&workers)
err := nbio.acquire_thread_event_loop()
defer nbio.release_thread_event_loop()
assert(err == nil)
server, listen_err := nbio.listen_tcp({nbio.IP4_Any, 1234})
assert(listen_err == nil)
nbio.accept_poly(server, &workers, on_accept)
err = nbio.run()
assert(err == nil)
on_accept :: proc(op: ^nbio.Operation, workers: ^thread.Pool) {
assert(op.accept.err == nil)
nbio.accept_poly(op.accept.socket, workers, on_accept)
thread.pool_add_task(workers, context.allocator, do_work, new_clone(Connection{
loop = op.l,
socket = op.accept.client,
}))
}
do_work :: proc(t: thread.Task) {
connection := (^Connection)(t.data)
// Imagine CPU intensive work that's been ofloaded to a worker thread.
time.sleep(time.Second * 1)
nbio.send_poly(connection.socket, {transmute([]byte)string("Hellope!\n")}, connection, on_sent, l=connection.loop)
}
on_sent :: proc(op: ^nbio.Operation, connection: ^Connection) {
assert(op.send.err == nil)
// Client got our message, clean up.
nbio.close(connection.socket)
free(connection)
}
}
**Handle and socket association**:
Most platforms require handles (files, sockets, etc.) to be explicitly
associated with an event loop or configured for non-blocking/asynchronous
operation.
On some platforms (notably Windows), this requires a specific flag at open
time (`.Non_Blocking` for `core:os`) and association may fail if the handle was
not created correctly.
For this reason, prefer `open` and `create_socket` from this package instead.
`associate_handle`, `associate_file`, and `associate_socket` can be used for
externally opened files/sockets.
**Offsets and positional I/O**:
Operations do not implicitly use or modify a handles internal file
offset.
Instead, operations such as `read` and `write` are *positional* and require
an explicit offset.
This avoids ambiguity and subtle bugs when multiple asynchronous operations
are issued concurrently against the same handle.
**Contexts and callbacks**:
The `context` inside a callback is *not* the context that submitted the
operation.
Instead, the callback receives the context that was active when the event
loop function (`tick`, `run`, etc.) was called.
This is because otherwise the context would have to be copied and held onto for
each operation.
If the submitting context is required inside the callback, it must be copied
into the operations user data explicitly.
Example:
nbio.timeout_poly(time.Second, new_clone(context), proc(_: ^Operation, ctx: ^runtime.Context) {
context = ctx^
free(ctx)
})
**Callback scheduling guarantees**:
Callbacks are guaranteed to be invoked in a later tick, never synchronously.
This means that the operation returned from a procedure is at least valid till
the end of the current tick, because an operation is freed after it's callback
is called. Thus you can set user data after an execution is queued, or call
`remove`, removing subtle "race" conditions and simplifying control flow.
**Why does the design of this package use callbacks?**
Callbacks are the simplest interface an event loop can reasonably expose: "Run
this when the operation completes". This means the loop itself doesnt need to
know how the result is consumed (You could give control of the entire loop to
the user, but that comes with all the problems in that).
Other mechanisms can be built on top of this, such as coroutines or even a queue
you comsume at your own time.
The choice of one `Operation` type and one callback type for all the operations
is partly so it is easy to bind into Lua-like or native coroutines.
Callbacks also allow multiple independent users to share the same event loop.
A package can register its own operations, and application code can register
others, without either seeing or handling the others completions.
*/
package nbio