Ch11.6: Mutex I/O
Thread-Safe I/O Overview
When multiple threads need to write to or read from the same file, you must synchronize
access to avoid data races and interleaved output. fast_io provides
lockable file variants that contain an internal mutex, making all I/O operations
on them thread-safe without requiring you to manage external synchronization.
Important: These lockable types only provide thread safety
(synchronization within a single process). They do not provide
process safety (synchronization across multiple processes). If multiple
processes need to write to the same file, consider using ::fast_io::native_file
directly, where each operation becomes a syscall — the OS may provide atomicity
guarantees for certain operations (e.g., O_APPEND writes on POSIX).
Note that if the OS provides atomicity guarantees for syscalls, those are also thread-safe
within a process. However, buffered output types accumulate data in user-space buffers
before flushing, which can break atomicity even if individual syscalls are atomic. The
order of outputs can also become chaotic because buffering splits logical units across
multiple flushes. For true atomic writes (both thread-safe and process-safe with
guaranteed ordering), use unbuffered ::fast_io::native_file.
Lockable File Types
For each standard buffered file type, there is a corresponding lockable variant:
-
::fast_io::ibuf_file_lockable— buffered input file with internal mutex -
::fast_io::obuf_file_lockable— buffered output file with internal mutex -
::fast_io::iobuf_file_lockable— buffered input/output file with internal mutex
These types behave exactly like their non-lockable counterparts. By default, every call to
scan, print, or println acquires the internal
mutex before performing I/O and releases it afterward. This guarantees that operations
from different threads do not interleave.
For better performance when performing multiple operations, you can manually lock the mutex, obtain the unlocked handle, and use that for all your operations. This avoids the overhead of acquiring and releasing the mutex for each individual operation.
Using Lockable Files
Here is an example showing multiple threads writing to a shared output file safely:
#include <fast_io.h>
#include <fast_io_device.h>
#include <thread>
int main()
{
using namespace ::fast_io::iomnp;
::fast_io::obuf_file_lockable obfl(u8"shared_output.txt");
auto writer = [&obfl](::std::size_t id, ::std::size_t count)
{
for (::std::size_t i{}; i < count; ++i)
{
println(obfl, "Thread ", id, " iteration ", i);
}
};
::std::jthread t1(writer, 1zu, 100zu);
::std::jthread t2(writer, 2zu, 100zu);
::std::jthread t3(writer, 3zu, 100zu);
// jthread automatically joins at end of scope
}
In this example, three threads concurrently print to the same obuf_file_lockable.
Because each println call acquires the internal mutex, the output lines never
interleave — each line is written atomically.
::fast_io::mutex for Manual Locking
If you need to protect a sequence of operations as a single atomic unit, or if you want
to use an unbuffered/non-lockable file type in a thread-safe manner, you can use
::fast_io::mutex directly. This is a portable mutex type provided by
fast_io.
#include <fast_io.h>
#include <fast_io_device.h>
#include <thread>
::fast_io::mutex mtx;
int main()
{
using namespace ::fast_io::iomnp;
::fast_io::obuf_file obf(u8"manual_lock_output.txt");
auto writer = [&](::std::size_t id, ::std::size_t count)
{
for (::std::size_t i{}; i < count; ++i)
{
::fast_io::io_lock_guard guard(mtx);
print(obf, "Thread ");
print(obf, id);
print(obf, " line ");
println(obf, i);
// guard releases mutex when it goes out of scope
}
};
::std::jthread t1(writer, 1zu, 50zu);
::std::jthread t2(writer, 2zu, 50zu);
// jthread automatically joins at end of scope
}
::fast_io::io_lock_guard locks the mutex upon construction and releases
it upon destruction (RAII). This lets you protect a group of related I/O calls as a
single critical section, ensuring they appear as one uninterrupted block.
When to Use Lockable Files vs. Manual Mutex
Use lockable file types (*_lockable) when:
- Each individual
print/println/scancall should be atomic. - You want a simple, drop-in thread-safe file with minimal boilerplate.
Use ::fast_io::mutex with ::fast_io::io_lock_guard when:
- You need to protect a multi-step sequence of operations as a single atomic group.
- You are working with non-lockable file types and need external synchronization.
- You need to synchronize I/O with other shared state.