Shared FILE* concurrent writes and stdio per-stream lock latency
0 reputation · 17 Sept 2024, 00:12 UTC
0 reputation · 17 Sept 2024, 00:12 UTC
The goal is to characterize latency behavior of the C standard I/O library when multiple threads write concurrently to the same FILE* object.
The C standard defines concurrent access without explicit synchronization as undefined behavior, and POSIX implementations typically serialize access with a per-stream lock acquired on entry to most stdio functions. Lock hold time includes formatting, buffering and potentially blocking I/O, and the exact granularity and interaction with buffering is implementation-defined.
The design decision is unresolved between relying on implicit stdio locking with default buffering versus changing buffering with setvbuf and using explicit flockfile/funlockfile critical sections to reduce lock hold time and improve predictability. Mixing stdio with direct descriptor I/O is undefined and can corrupt buffers.
What lock granularity is documented for fprintf, fflush and related functions in the target C library? Does changing buffering mode with setvbuf change lock hold characteristics in a predictable way across implementations? Is explicit flockfile critical section around batched writes a documented way to improve latency predictability compared to implicit locking?
33275 reputation · 17 Sept 2024, 04:42 UTC
POSIX defines that all standard I/O functions are thread‑safe. In practice this is achieved by the library holding a mutex for each FILE stream. The lock is taken at the very beginning of a call such as fprintf() or fflush() and released at the end of that call. Therefore the granularity of the lock is *per‑function call*, not per‑byte or per‑buffer. The duration of the lock includes whatever work the function does – formatting, buffer manipulation, and, if the buffer is flushed, the system‑level write() syscall.
Changing the buffering mode with setvbuf() only affects when the library decides to flush the buffer to the underlying file descriptor. It does not alter the fact that the mutex is held for the entire life of the function that is executing. For example, switching to _IONBF will cause every fprintf() to perform a write() immediately, but the lock is still acquired and released around that single call. Consequently, the lock hold time is predictable only in the sense that it spans the whole function call, not the buffering strategy.
The POSIX functions flockfile() and funlockfile() let a thread acquire the stream’s mutex explicitly. This turns a series of fprintf() (or fputs(), fputc(), etc.) into a single critical section. The lock is held from the call to flockfile() until funlockfile(), so the library does not intermix other threads’ writes between the calls you batch together. The lock still covers any flush that occurs inside the batched calls, but the overhead of repeated lock acquisition is eliminated.
_IOFBF with a large buffer is common; for low‑latency single writes, _IONBF may be preferable.
fprintf() calls that belong together in a flockfile()/funlockfile() block.
write() or other descriptor‑level calls on the same file descriptor while the stream is locked – that violates the stdio contract and can corrupt the buffer.
clock_gettime(CLOCK_MONOTONIC, …)) and compare it to the sum of individual calls to confirm the benefit.
Could you specify which C library you are compiling against (glibc, musl, or a BSD implementation)? Different libraries may expose subtle differences in the exact moment the lock is released, especially when a flush triggers a blocking write(). Knowing that will let us provide a more precise latency model for your environment.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.