Skip to content
CryoCryo home
Stdlibfuture

blocking

import std::future::blocking; · source

An off-executor home for work that cannot be polled. A Reactor lets a future sleep on something the OS can report readiness for; some work has no such report. getaddrinfo is a synchronous call with no non-blocking form on either platform, and a regular file is always "ready" as far as epoll is concerned, so neither can be driven by the readiness reactor. Running one directly inside a poll stalls an executor worker for the whole call — on a single-threaded Executor, the whole runtime.

The pool is the standard answer: a small set of OS threads that run such calls off the executor and fire a Waker when they finish. The call still blocks a thread; it blocks one that nobody is polling on.

BlockingPool

type struct BlockingPool {
    mtx:      u8[40];
    cv:       u8[48];
    head:     BlockingJob*;
    tail:     BlockingJob*;
    shutdown: boolean;
    live:     u32;
    idle:     u32;
    max:      u32;
    tids:     u64*;

    static start() -> BlockingPool*;
    static with_max(max: u32) -> BlockingPool*;
    static stop(p: BlockingPool*) -> void;
    static free(p: BlockingPool*) -> void;
    static current() -> BlockingPool*;
    static set_current(p: BlockingPool*) -> void;
    submit(mut &this, job: BlockingJob*) -> boolean;
    take(mut &this) -> BlockingJob*;
}

An Executor starts one and its workers set it as the thread's ambient pool through set_current; current() is how a future finds it. submit queues a job and returns whether a thread will run it.

BlockingJob

type struct BlockingJob {
    rc:     Atomic<u32>;
    status: Atomic<u8>;

    run_fn: (u8*) -> void;
    drop_fn: (u8*) -> void;

    waker: Waker;
    next:  BlockingJob*;
    pool:  BlockingPool*;

    static init(job: BlockingJob*, run_fn: (u8*) -> void, drop_fn: (u8*) -> void) -> void;
    arm(mut &this, w: Waker) -> u8;
    finish(mut &this, status: u8) -> void;
    retain(mut &this) -> void;
    release(mut &this) -> void;
}

The shape of a job

The pool's queue is homogeneous, so a job is type-erased the same way a task and a waker are — a manual vtable of non-capturing function pointers. A user of the pool declares a struct whose first field is a BlockingJob and hands that field's address to submit; run_fn casts the address back to the concrete block and finds its payload beside the header:

type struct ResolveJob {
    job:  BlockingJob;   // first: `submit` is handed &this.job
    host: String;        // the request, owned by the block
    out:  Result<..>;    // written by `run_fn`, read once it finishes
}

init fills the header with the run and drop functions; the future's poll calls arm(waker) to say who to wake and reads the status it returns; finish is what the pool thread calls when run_fn returns. retain and release are the reference count that lets the future and the pool thread disagree about who finishes last — a cancelled future releases its side and the pool frees the block when the call eventually returns.

Growth

Threads are created on demand, never in advance, and only when no thread is already idle: a pool that grew on every submit would reach its ceiling under a burst one thread could have absorbed. They are not reaped while the pool lives — a blocking pool's threads are asleep in a syscall, not consuming CPU, and an executor's lifetime is the natural bound. Past max threads, jobs queue. Executor::with_threads_and_blocking sets the cap.

net::dns::Resolve, process::ChildWait, and the pipe drain behind Command::collect are the in-tree users.