fseek在多线程中不可用,因其操作FILE*共享的内部偏移指针,引发竞态;应改用pread,它以显式offset参数实现无锁并行读取。

为什么 fseek 在多线程中不能直接用指针加速偏移定位
因为 fseek 操作的是文件流的共享内部位置指针(FILE* 的 __offset 或类似字段),不是用户可控的独立变量。多个线程对同一 FILE* 调用 fseek 会相互覆盖、竞争,导致定位错乱——这不是“慢”,而是**根本不可用**。
真正能并行定位的,是底层系统调用层面的偏移控制,比如 pread,它把文件描述符、缓冲区、偏移量三者打包成一次原子读,不依赖流状态。
- 别试图用
int*或off_t*去“缓存”或“共享”当前偏移——这解决不了竞态,反而掩盖问题 -
std::ifstream的seekg同样基于fseek,多线程共用一个流对象时行为未定义 - 如果你已经用
open()获取了int fd,就不要再包一层FILE*,否则失去控制权
如何用 pread 实现无锁偏移定位 + 并行读取
pread 是 POSIX 标准函数,接受显式 offset 参数,不改变文件描述符的当前偏移,天然适合多线程按块读取。
关键点在于:每个线程只负责自己那块数据的起始偏移,传给 pread 即可,完全不需要维护或同步“当前偏移”状态。
立即学习“C++免费学习笔记(深入)”;
- 先用
stat()获取文件总大小,划分逻辑块(如每块 64KB) - 每个线程计算自己的
offset = block_id * block_size,作为pread第四个参数 - 确保每个线程有独立的缓冲区(
char* buf),避免写冲突 - 错误检查必须做:
pread返回值可能小于请求长度(到 EOF),也可能是 -1(失败)
ssize_t n = pread(fd, buf, len, offset);
if (n == -1) {
perror("pread");
return;
}
// 注意:n 可能 < len,需处理短读
mmap + 指针算术比 pread 更快?什么情况下值得切
是的,mmap 把文件映射为内存地址,后续访问就像读数组——用 char* 加偏移就能直接取数据,零系统调用开销。但它不是“用指针优化 fseek”,而是绕开了 I/O 定位这个概念本身。
适用前提是:文件可完整映射(不超虚拟内存限制)、随机访问密集、且你愿意承担 mmap 初始化延迟和页错误开销。
- 映射后拿到的是
void*,强制转为const char*才能做指针算术:const char* base = static_cast<const char>(addr)</const> - 访问
base[offset]前,必须保证offset在映射范围内(offset ),否则触发 SIGBUS - 大文件(>数 GB)映射可能失败或拖慢进程启动;小文件(pread 直接
-
mmap不自动刷新磁盘内容,修改后需msync,纯读场景可忽略
跨平台时怎么处理 Windows 上没有 pread 和 mmap 的问题
Windows 没有 pread,但有等价的 ReadFile + LARGE_INTEGER 偏移参数;也没有标准 mmap,但可用 CreateFileMapping + MapViewOfFile 实现类似语义。
不要手写条件编译宏堆砌逻辑。推荐封装一层轻量 I/O 接口:
- 定义结构体
file_reader,含fd(POSIX)或hFile(Win32)成员 - 提供统一函数
read_at(int64_t offset, void* buf, size_t len),内部按平台分发 - 在 Windows 上用
SetFilePointerEx+ReadFile组合也能做到无锁,但要注意SetFilePointerEx修改句柄位置——所以必须加LOCKFILE_EXCLUSIVE_LOCK或改用ReadFile的lpOverlapped参数指定偏移(需开启异步句柄)
最容易被忽略的一点:Windows 的文件句柄默认不支持重叠 I/O,创建时必须带 FILE_FLAG_OVERLAPPED,否则 lpOverlapped 会被忽略,退化为阻塞行为。


















