# How does concurrency work in hxprobe? ## Question Selection in `hxprobe/hxprobe/cli.py:448-449`: ```python with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as ex: all_results = list(ex.map(_probe, urls)) ``` > explain how concurrency works in hxprobe ## Answer There are two independent levels of repetition: 1. **Across URLs** — parallel, via a thread pool. 2. **Across samples of the same URL** (`-n`/`--count`) — sequential, within a single thread. ### Worker count `cli.py:440-443` ```python workers = ns.concurrency if workers <= 0: workers = min(len(urls), 8) workers = max(1, min(workers, len(urls))) ``` `-c`/`--concurrency` picks the pool size; `0` (default) means "auto" → `min(len(urls), 8)`. The final clamp guarantees `1 ≤ workers ≤ len(urls)` — never more threads than there are URLs to probe, never zero. ### The pool itself `cli.py:445-449` ```python def _probe(url: str) -> tuple[list[Result], list[Result]]: return _run_samples(url, count, opts) with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as ex: all_results = list(ex.map(_probe, urls)) ``` One task per URL is submitted to the pool. `_probe` delegates to `_run_samples`, `cli.py:64-69`: ```python def _run_samples(url: str, count: int, opts: Options) -> tuple[list[Result], list[Result]]: succeeded, failed = [], [] for _ in range(count): r = measure(url, opts) (failed if r.err else succeeded).append(r) return succeeded, failed ``` So within one URL's thread, the `count` samples run **one at a time** — never concurrently. This is deliberate: if N samples fired at the same host in parallel, they'd contend for the same TCP/TLS handshake path and connection setup, and the resulting min/avg/max per phase would reflect that contention rather than the host's actual latency. Keeping samples sequential is what makes `-n`'s statistics meaningful. ### Why threads work here despite the GIL `measure()` does blocking socket/SSL I/O — `socket.connect()`, `.recv()`, `ssl.wrap_socket()`, etc. (see the custom `_TimingBackend`/`_TimingStream` in `probe.py`). CPython releases the GIL around blocking syscalls, so N threads genuinely overlap in wall-clock time waiting on the network, even though only one thread executes Python bytecode at once. This is I/O-bound concurrency, not CPU parallelism — threads are the right tool, not `asyncio` or multiprocessing. ### Ordering guarantee `ex.map(_probe, urls)` returns results in the *same order as the input `urls`*, regardless of which thread finishes first — that's a documented property of `Executor.map`. That's why `list(ex.map(...))` can be zipped directly against `urls` afterward (`cli.py:454`) to produce deterministic text/JSON output order, even though the underlying probes complete out of order. ### No shared mutable state / no locks needed Each thread's `_probe` call returns its own `(succeeded, failed)` tuple; nothing is written to a shared structure until back in the main thread after the `with` block exits (which also blocks until every submitted task completes, since `ThreadPoolExecutor.__exit__` calls `shutdown(wait=True)`). The worst-exit-code accumulation and JSON building (`cli.py:451-472`) then run single-threaded over `all_results`.