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:

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:

Use ::fast_io::mutex with ::fast_io::io_lock_guard when: