Sync, async & concurrency
The runtime itself makes no decision here — as the overview
page covers, it's plain sync Rust, no threads, no Arc
anywhere inside it. Everything below is a pattern for your code, not a
feature of comline-runtime.
Plain sync — the common case
Most of what's shown elsewhere in this guide is already this: own a
Client (or a Dispatcher) directly, call it, block, move on. If your
program is single-threaded, or each thread owns its own connection (the
per-connection-thread pattern on the
server side), there's nothing further to do.
Sharing a Client across threads
Client::call takes &mut self, so sharing one Client across threads
needs a lock:
use std::sync::{Arc, Mutex};
let client = Arc::new(Mutex::new(MailClient::connect(transport)?));
let c = Arc::clone(&client);
thread::spawn(move || {
let body = c.lock().unwrap().send_message("from thread 2".into()).unwrap();
});
This isn't just "the safe option because the borrow checker demands it" —
it's a reasonable fit for what's actually happening underneath. One call
is outstanding at a time on
any single Client regardless of locking, since a call borrows the
client until its response is consumed. The mutex isn't adding an
artificial bottleneck on top of a runtime that could otherwise pipeline
several calls at once — there's nothing to pipeline yet either way.
If you need actual concurrent calls in flight, that means several
connections (several Clients, each with its own transport), not several
threads contending for one.
Calling from async code
There's no async client or server today — the crate's own stated
intent (quoted on the overview page) is an async layer
wrapping the sync core behind the std feature, later. Until that ships,
calling a sync Client from inside an async task means not blocking the
reactor:
let client = Arc::new(Mutex::new(client));
let c = Arc::clone(&client);
let body = tokio::task::spawn_blocking(move || {
c.lock().unwrap().send_message("hi".into())
})
.await
.unwrap()?;
spawn_blocking (or your executor's equivalent — async-std's
spawn_blocking, a dedicated thread pool) runs the blocking call on a
thread meant for blocking work, so it doesn't stall the async runtime's
other tasks while it waits on the network. Treat this as the bridge until
an async layer exists, not a permanent design — don't build deep
abstractions on top of spawn_blocking that you'll need to unwind later.
Serving several connections concurrently
Covered from the server side in Client &
server: one thread per accepted connection
is the straightforward version, each with its own Dispatcher (or a
shared, Sync implementation type if your provider logic itself needs
shared state — ordinary Rust at that point, nothing runtime-specific).