第11.6章: 互斥 I/O

English

线程安全 I/O 概览

当多个线程需要读写同一个文件时,你必须同步访问,以避免数据竞争和交错的输出。fast_io 提供可加锁的文件变体,它们内部含有一个互斥量,使它们上面的所有 I/O 操作都是线程安全的,而无需你自己管理外部同步。

重要:这些可加锁类型只提供线程安全(单个进程内的同步),不提供进程安全(多个进程之间的同步)。如果多个进程需要写同一个文件,考虑直接使用 ::fast_io::native_file,此时每次操作都变成一次系统调用 — 操作系统可能对某些操作提供原子性保证(例如 POSIX 上带 O_APPEND 的写入)。

注意,如果操作系统为系统调用提供了原子性保证,那么在进程内它们也是线程安全的。然而带缓冲的输出类型会先把数据积累在用户态缓冲区里再刷新,即使单个系统调用是原子的,这也可能破坏原子性。输出的顺序也可能变得混乱,因为缓冲会把逻辑单元拆到多次刷新中。若需要真正原子的写入(既线程安全、又进程安全且顺序有保证),请使用不带缓冲的 ::fast_io::native_file。

可加锁的文件类型

每种标准带缓冲文件类型都有一个对应的可加锁变体:

这些类型的行为与它们不可加锁的对应物完全一样。默认情况下,每次调用 scan、print 或 println 都会在执行 I/O 之前取得内部互斥量,之后再释放它。这保证来自不同线程的操作不会交错。

当你要连续执行多次操作时,为了更好的性能,可以手动锁定互斥量,取得未加锁的句柄,并用它完成所有操作。这样避免了为每一次单个操作获取和释放互斥量的开销。

使用可加锁文件

下面是一个多个线程安全地写入同一个输出文件的例子:


#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
}
      

在这个例子里,三个线程并发地打印到同一个 obuf_file_lockable。因为每次 println 调用都会获取内部互斥量,输出行从不交错 — 每一行都是原子地写出的。

用 ::fast_io::mutex 手动加锁

如果你需要把一串操作保护为单个原子单元,或者想以线程安全的方式使用不带缓冲/不可加锁的文件类型,可以直接使用 ::fast_io::mutex。这是 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 在构造时锁定互斥量,在析构时释放(RAII)。这让你能把一组相关的 I/O 调用保护成一个临界区,确保它们作为一个不间断的整体出现。

什么时候用可加锁文件,什么时候用手动互斥量

在下列情况下使用可加锁文件类型(*_lockable):

在下列情况下使用 ::fast_io::mutex 配合 ::fast_io::io_lock_guard: