在Linux上,C语言中的stdout是否是线程安全的?

50

在Linux上,使用printf向标准输出(stdout)写入内容是否线程安全?使用底层的write命令呢?

5个回答

64

在C标准中并没有明确规定这一点,因为它取决于您所使用的C标准库实现。实际上,C标准甚至没有提及线程,因为某些系统(例如嵌入式系统)不支持多线程。

在GNU实现中(glibc),大多数与FILE*对象有关的高级stdio函数是线程安全的。那些不安全的函数通常在它们的名称中加有unlocked(例如getc_unlocked(3))。但是,线程安全性在每个函数调用级别上:例如,如果您对printf(3)进行多次调用,则保证每个调用都会原子输出,但是其他线程可能会在您调用printf()之间打印东西。如果您想确保一个I/O调用序列被原子输出,您可以使用一对flockfile(3)/funlockfile(3)调用来锁定FILE句柄。请注意,这些函数是可重入的,因此您可以在它们之间安全地调用printf(),而且不会导致死锁,即使printf()本身调用了flockfile()

诸如write(2)之类的低级I/O调用应该是线程安全的,但我不能完全确定 - write()会向内核发出系统调用以执行I/O。这样做的方式取决于您正在使用的内核。它可能是sysenter指令,也可能是旧系统上的int(中断)指令。一旦进入内核,内核就要确保I/O是线程安全的。在我刚刚对Darwin Kernel Version 8.11.1进行的测试中,write(2)似乎是线程安全的。


27
这个答案忽略了问题标记为Unix/Linux。POSIX要求stdio是线程安全的,这很不幸,因为它会降低性能,并且没有实际的方法可以从多个线程操作同一个FILE(数据将混杂在一起;原子性仅在字符级别)。 - R.. GitHub STOP HELPING ICE
1
有时,如果输出是交错的,例如通过多个线程通过printf进行记录,这是完全可以接受的。 - nob
1
@couling 我认为他的意思是线程安全是无用的,因为所有东西都会交错在一起 - 除非你使用显式的f[un]lockfile。 - Adrian Ratnapala
我正在使用更新的内核和amd64 Linux。当我将setvbuf设置为_IONBF以用于stdout,并使用CLONE_VM调用clone()时,会导致子进程中的printf出现分段错误。即使手动为每个进程分配自己的缓冲区,问题仍然存在。我仍在努力追踪发生的内存重叠问题。这种情况发生与或没有CLONE_IO和/或CLONE_IO标志。 - sol

26
无论您如何定义"线程安全",都取决于您的定义。 POSIX 要求 stdio 函数使用锁定,因此,如果您同时从多个线程使用 printf,您的程序将不会崩溃、损坏 FILE 对象状态等。但是,所有 stdio 操作都在重复调用 fgetcfputc 的术语中正式指定,因此不存在较大规模的原子性保证。换句话说,如果线程 1 和 2 尝试同时打印 "Hello\n""Goodbye\n",则不能保证输出是 "Hello\nGoodbye\n""Goodbye\nHello\n"。它可能是 "HGelolodboy\ne\n"。实际上,大多数实现将为整个高级写入调用获取单个锁,仅因为这样更有效率,但是您的程序不应假设如此。可能存在一些角落情况未执行此操作;例如,实现可以完全在无缓冲流上省略锁定。
编辑:有关原子性的上述文本是不正确的。POSIX 保证了所有 stdio 操作是原子的,但保证隐藏在 flockfile 的文档中:http://pubs.opengroup.org/onlinepubs/9699919799/functions/flockfile.html

所有引用 (FILE *) 对象的函数应该表现得好像它们在内部使用 flockfile() 和 funlockfile() 来获取这些 (FILE *) 对象的所有权。

您可以使用 flockfileftrylockfilefunlockfile 函数自己实现大于单个函数调用的原子写入。

17

从线程安全的角度来看,它们都能够保证当多个线程在同一个文件描述符上调用它们时,应用程序不会崩溃。但是,如果没有进行一些应用层面的锁定,那么写入的内容可能会交错。


10
自从这个问题被提出(并且上次回答)以来,C语言有了新的标准。
C11现在具有多线程支持,并解决了流的多线程行为:
§7.21.2 Streams
¶7 每个流都有一个关联锁,用于防止多个执行线程访问流时发生数据竞争,并限制多个线程执行的流操作的交错。一次只能有一个线程持有此锁。该锁是可重入的:单个线程可以同时多次持有锁。
¶8 所有读取、写入、定位或查询流位置的函数在访问之前会锁定流。当访问完成后,它们会释放与流相关联的锁。
因此,使用C11线程的实现必须保证使用printf是线程安全的。

我一开始并不清楚是否保证了原子性(即没有交错1),因为标准中讲到的是“限制”交错,而不是“防止”,这与数据竞争的强制要求有所不同。

我倾向于认为它是得到了保证的。标准中讲到的是“限制”交错,因为某些不改变结果的交错仍然是允许的;例如,fwrite写入一些字节,fseek回到更多位置,然后再次fwrite直到原始偏移量,以便两个fwrite在一起。实现可以重新排序这两个fwrite并将它们合并成一个写操作。


1:请参考R的回答中的删除线文本示例。


6

它是线程安全的;printf 应该是可重入的,您不会在程序中导致任何奇怪或损坏的情况。

您无法保证来自一个线程的输出不会从另一个线程的输出中间开始。如果您关心这一点,您需要开发自己的锁定输出代码以防止多次访问。


所有对printf的调用都可能使用相同的缓冲区来构建字符串。 许多实现还在scanf和printf之间共享缓冲区,这可能会导致一些奇怪的依赖于调试的错误。 - Martin Beckett
3
我不知道其他人怎么做,但是 GNU C 库默认情况下是线程安全的,所以它不会使用相同的缓冲区。 - Adam Hawes
2
我认为printf不是可重入的,请参见https://dev59.com/QG865IYBdhLWcg3wLrlE。 - Yu Hao

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