第11.6章: 互斥 I/O
线程安全 I/O 概览
当多个线程需要读写同一个文件时,你必须同步访问,以避免数据竞争和交错的输出。fast_io 提供可加锁的文件变体,它们内部含有一个互斥量,使它们上面的所有 I/O 操作都是线程安全的,而无需你自己管理外部同步。
重要:这些可加锁类型只提供线程安全(单个进程内的同步),不提供进程安全(多个进程之间的同步)。如果多个进程需要写同一个文件,考虑直接使用 ::fast_io::native_file,此时每次操作都变成一次系统调用 — 操作系统可能对某些操作提供原子性保证(例如 POSIX 上带 O_APPEND 的写入)。
注意,如果操作系统为系统调用提供了原子性保证,那么在进程内它们也是线程安全的。然而带缓冲的输出类型会先把数据积累在用户态缓冲区里再刷新,即使单个系统调用是原子的,这也可能破坏原子性。输出的顺序也可能变得混乱,因为缓冲会把逻辑单元拆到多次刷新中。若需要真正原子的写入(既线程安全、又进程安全且顺序有保证),请使用不带缓冲的 ::fast_io::native_file。
可加锁的文件类型
每种标准带缓冲文件类型都有一个对应的可加锁变体:
-
::fast_io::ibuf_file_lockable— 内部带互斥量的缓冲输入文件 -
::fast_io::obuf_file_lockable— 内部带互斥量的缓冲输出文件 -
::fast_io::iobuf_file_lockable— 内部带互斥量的缓冲输入/输出文件
这些类型的行为与它们不可加锁的对应物完全一样。默认情况下,每次调用 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):
- 每次单独的
print/println/scan调用应当是原子的。 - 你想要一个简单、可直接替换的线程安全文件,样板代码最少。
在下列情况下使用 ::fast_io::mutex 配合 ::fast_io::io_lock_guard:
- 你需要把多步操作序列保护成单个原子组。
- 你在使用不可加锁的文件类型,需要外部同步。
- 你需要把 I/O 与其他共享状态一起同步。