一个程序能否同时在同一个文件指针(FILE*)上调用fflush()函数?

45

如果多个线程同时在相同的FILE*变量上调用fflush(),会发生什么不良情况(如未定义行为、文件损坏等)?

澄清:我不是指并发写文件,而是指并发刷新文件。

这些线程不会同时读取或写入文件(它们只在关键区域内一个接一个地写入文件)。他们只会在关键区域外部进行刷新,以便更快释放关键区域,让其他线程做其他工作(除了文件写入)。

尽管可能存在一个线程正在写入文件(在关键区域内),而另一个线程/线程正在刷新文件(在关键区域外)的情况。


这听起来似乎是从设计角度提出的问题,而不是试图消除错误解释 - 也许值得更明确地表达。 - PJTraill
1
鉴于FILE是线程安全的答案,一个负面后果可能是一个线程被迫等待另一个线程刷新,尽管这只有在您需要相当高的性能时才会有影响。使用像flockfileflock这样的函数可能会导致死锁,但在您描述的情况下可能不会发生。 - PJTraill
@PJTraill,没问题:如果一个线程正在刷新,而另一个线程写入一些内容,然后发现它也需要刷新,那么它所能做的就是等待第一个线程将FILE*中的所有内容都刷新到一致状态(包括第二个线程写入的内容 - 这取决于竞争),然后第二个线程应确保它刚刚写入的内容也被刷新。当然,使用专用线程进行异步刷新有更好的方法,但这更加复杂,因此超出了范围。 - Serge Rogatch
您似乎假设刷新只会延迟尝试刷新的其他线程,但我理解 2501 回答中的引用是,由于每个流只有 单个 锁定,因此它也会延迟尝试写入的线程—这可能是不希望发生的! - PJTraill
5个回答

40

C语言中的流(Streams)是线程安全的。在访问流之前,需要锁定该流的函数。

fflush函数是线程安全的,并且只要流是输出流或更新流,就可以在任何线程中调用它。


1 根据当前标准,即C11。

2 (引自:ISO/IEC 9899:201x 7.21.3 Streams 7)
每个流都有一个相关联的锁,用于在多个执行线程访问流时防止数据竞争,并限制多个线程执行的流操作。一次只能有一个线程持有此锁。锁可重入:单个线程可以同时多次持有锁。

3 (引自:ISO/IEC 9899:201x 7.21.3 Streams 8)
所有读取、写入、定位或查询流位置的函数在访问流之前会锁定该流。完成访问后,它们会释放与流关联的锁。 锁可重入:单个线程可以同时多次持有锁。

4 (引自:ISO/IEC 9899:201x 7.21.5.2 The fflush function 2)
如果流指向的是最近一次操作不是输入的输出流或更新流,则fflush函数将导致该流的所有未写数据传递到主机环境以写入文件;否则,行为是未定义的。


1
MSVC编译器在x86/x86_64 Windows上是否遵循这些规则? - Serge Rogatch
8
@cat VS甚至无法符合C99标准。 - 2501
2
C语言的流并不总是安全的。我记得曾经追踪了一个它们不安全的bug。尽管如此,如果你今天遇到这样的bug,请向你的编译器供应商报告。 - Joshua
3
微软的Visual C++旨在成为C++编译器,而非C编译器。据我所知,最近版本的Visual C++支持C++14。 - user253751
1
我们在使用MSVC 2015编译的代码中,同时使用fwritefflush对同一个FILE进行操作,并没有出现任何问题。写入操作在输出文件中被很好地交错执行。 - rustyx
显示剩余7条评论

12

操作字符流(由指向FILE类型对象的指针表示)的POSIX.1和C语言函数有着“必须”实现可重入性的要求(参见ISO/IEC 9945:1-1996,§8.2)。

这个要求有一个缺点;为了实现可重入性,必须在函数实现中构建同步机制,这会带来相当大的性能损失。POSIX.1c通过引入以下C语言标准I/O函数的高性能、非可重入(潜在不安全)版本,在可重入性(安全性)与性能之间进行权衡:getc()、getchar()、putc()和putchar()。非可重入版本的名称是getc_unlocked()等,以突出它们的不安全性。

需要注意的是,许多流行的系统(包括Windows和Android)并不符合POSIX标准。


▲ 对于“_不符合POSIX标准_”,你是指fflush不是线程安全的,他们没有实现POSIX.1c标准,还是他们不声称符合该标准? 一个列出各个平台(包括版本信息)在这方面符合性的清单(或链接)可能是一个有用的资源。 - PJTraill
我不知道Google是否承诺fflush是线程安全的,即使它当前是线程安全的(参见 https://sourceforge.net/u/lluct/me722-cm/ci/86c46fc79a15bc9500fbb47241a15c148b8abb01/tree/bionic/libc/stdio/fflush.c),但是Microsoft至少在Windows上保证线程安全(参见 https://msdn.microsoft.com/en-us/library/9yky46tz.aspx)。 - geocar

9
不应在输入流上调用fflush(),这会导致未定义的行为,因此我将假设流以写入或更新模式打开。
如果流以更新模式("w+""r+")打开,则在调用fflush()时,最后一个操作不能是读取操作。由于流被异步地用于各种线程,如果你进行任何读取操作,要确保不会读取到不一致的数据,这将需要某种形式的进程间通信和同步机制或锁。仍然有一个有效的原因可以以更新模式打开文件,但请确保在启动fflush线程后不要再进行任何读取操作。 fflush()不会修改当前位置,它只会导致任何缓冲输出被写入系统。流通常受锁保护,因此在一个线程中调用fflush()不应该干扰其他线程执行的输出,但它可能会改变系统写入的时间。如果多个线程输出相同的FILE*,则交错发生的顺序是不确定的。此外,如果您在不同的线程中为同一流使用fseek(),则必须使用自己的锁来确保fseek()和随后的输出之间的一致性。
虽然看起来可以这样做,但可能不建议这样做。相反,在每个线程的写操作之后,在释放锁之前可以调用fflush()

1
很简单的答案,你可能做不到,因为该文件只有一个“当前位置”。你如何跟踪它?该文件是按顺序访问还是随机访问?在后一种情况下,您可以多次打开它(每个线程一个),并想出方法保持文件结构一致。

好的,我应该将这个添加到问题中:线程不会同时读取或写入文件(它们只在关键部分内写入文件,一次只有一个线程)。它们只在关键部分外部刷新,以更快地释放关键部分,让其他线程完成其他工作(除了文件写入)。 - Serge Rogatch
3
fflush() 不会修改当前位置。 - chqrlie
那么,考虑采用不同方式更容易实现,创建另一个工作线程(没有窗口、定时器、消息队列等),仅执行刷新操作(或者也可以包括写入?)。线程过程只包含一个带有SleepEx()命令的循环。其他线程将发送APC请求以进行写入,并且APC过程将执行它们(依次排队执行)。实现只需几行源代码,如果您已经不知道如何实现,您实际上不需要阅读太多内容。请参阅QueueUserAPC()函数的文档。 - Constantine Georgiou

0

实际答案似乎是流本身(应该)是线程安全的,但如果不是这种情况,您的问题可能是fflush在(锁定外部)发生,而另一个线程正在写入(在关键部分内)。

因此,我会与您编码的虚拟机模型一起工作,并将fflush()也放在关键部分中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接