Skip to content

[Proposal] - Application scheduled I/O execution #4903

Description

@avb-blizzard

Well met!

I would like to propose a draft API that allows embedders to run libzmq without spawning its internal I/O and reaper threads.

Motivation

Game engines already own a carefully scheduled main loop. These applications may want to avoid background threads, unpredictable scheduling, and explicitly decide when network progress is allowed to run within a frame. Existing APIs let applications configure the number, affinity, and priority of libzmq I/O threads, or poll application-facing socket readiness with zmq_poll / zmq_poller / ZMQ_FD. However, they do not let the host application disable libzmq-owned I/O/reaper threads while explicitly advancing internal transport progress within a host-controlled budget. For example, ZMQ_IO_THREADS=0 is only suitable for inproc://-only contexts.

The proposed model is:

  1. Create a context.
  2. Enable manual I/O mode before the first socket is created.
  3. Use normal libzmq socket APIs from the application loop.
  4. Explicitly call a zmq_ctx_step-style function each tick/frame to advance libzmq's internal I/O and reaper work.

This aims only to make internal progress manually (publicly) advanceable instead of owned by libzmq-created threads.

Sketch of possible API

zmq_ctx_set(ctx, ZMQ_MANUAL_IO_THREADS, 1);

// Initial shape for discussion only. A real API would accept a budget. 
zmq_ctx_step(ctx, timeout_ms); 
zmq_ctx_step_io(ctx, io_index, timeout_ms);
zmq_ctx_step_reaper(ctx, timeout_ms);

ZMQ_MANUAL_IO_THREADS would be a draft context option and must be set before the context starts.

Example application loop

while (running) {
    simulation->tick();

    zmq_recv(socket, buffer, sizeof buffer, ZMQ_DONTWAIT);
    zmq_send(socket, payload, payload_sz, ZMQ_DONTWAIT);
    // Could run here, or in a separate networking phase, or another engine-owned worker
    zmq_ctx_step(ctx, 0);

    gfx->render();
}

In a single-threaded manual loop, applications would generally use non-blocking or bounded socket operations so they do not block waiting for progress that only the next zmq_ctx_step* call can provide.

Sketch of budgeting considerations

A more full-bodied implementation could look something like this:

typedef struct zmq_ctx_step_budget_t {
    // Best-effort progress budget.
    uint64_t max_duration_ns;
    // 0 means no explicit event limit.
    uint32_t max_events;
    // 0 means no explicit mailbox-command limit.
    uint32_t max_commands; 
    // 0 means no explicit timer limit.
    uint32_t max_timers;
    uint32_t flags;
} zmq_ctx_step_budget_t;

typedef struct zmq_ctx_step_result_t {
    uint32_t events_processed;
    uint32_t commands_processed;
    uint32_t timers_processed;
     // flags may include MORE_WORK, IDLE, BUDGET_EXHAUSTED.
    uint32_t flags;
} zmq_ctx_step_result_t;

int zmq_ctx_step_budget(
    void *ctx,
    const zmq_ctx_step_budget_t *budget,
    zmq_ctx_step_result_t *result);

Open questions

  1. I would like maintainer feedback on the desired public contract.
  2. Is such a set of APIs compatible with the direction, philosophy of libzmq?
  3. What transports and poller backends would need coverage before this could be accepted as draft?
  4. I'm sure there will be more questions if this is seriously considered for contribution back upstream 😄

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions