printf
在Linux上使用线程安全写入stdout ?使用低级write
命令怎么样?
它没有由C标准指定 - 它取决于您对C标准库的实现.事实上,C标准甚至根本没有提到线程,因为某些系统(例如嵌入式系统)没有多线程.
在GNU实现(glibc
)中,stdio中处理FILE*
对象的大多数高级函数都是线程安全的.那些通常没有unlocked
名字的人(例如getc_unlocked(3)
).但是,线程安全性处于每个函数的调用级别:printf(3)
例如,如果您进行多次调用,则每个调用都保证以原子方式输出,但其他线程可能会在您的调用之间打印出来printf()
.如果要确保一系列I/O调用以原子方式输出,可以使用一flockfile(3)/funlockfile(3)
对调用来包围它们以锁定FILE
句柄.请注意,这些函数是可重入的,因此您可以安全地printf()
在它们之间调用,即使认为printf()
自己调用也不会导致死锁flockfile()
.
低级I/O调用write(2)
应该是线程安全的,但我不是百分之百确定 - write()
使系统调用内核来执行I/O. 这究竟是怎么发生取决于你正在使用什么内核.它可能是旧系统上的sysenter
指令或int
(中断)指令.进入内核之后,由内核决定I/O是否是线程安全的.在我刚刚使用Darwin内核版本8.11.1进行的测试中,write(2)
似乎是线程安全的.
你是否称它为"线程安全"取决于你对线程安全的定义.POSIX要求stdio
函数使用锁定,因此FILE
如果printf
从多个线程同时使用,程序不会崩溃,损坏对象状态等.然而,所有的stdio
操作在重复调用的术语正式指定fgetc
和fputc
,所以保证没有较大规模的原子.也就是说,如果线程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*)对象的所有权.
您可以使用flockfile
,ftrylockfile
和funlockfile
功能自己实现更大高于单功能调用原子写入.
它们都是线程安全的,如果多个线程在同一文件描述符上调用它们,应用程序将不会崩溃.但是,如果没有一些应用程序级别的锁定,写入的内容可能是交错的.
自从提出这个问题(并且最后回答)后,C得到了一个新的标准.
C11现在提供多线程支持并解决了流的多线程行为:
§7.21.2流
7每个流都有一个关联的锁,用于在多个执行线程访问流时阻止数据竞争,并限制多个线程执行的流操作的交错.一次只有一个线程可以保持此锁定.锁是可重入的:单个线程可以在给定时间多次保持锁.
8读取,写入,定位或查询流的位置的所有函数在访问流之前锁定流.当访问完成时,它们释放与流关联的锁.
因此,使用C11线程的实现必须保证使用printf
是线程安全的.
原子性(如没有交错1)是否得到保证,乍一看对我来说并不是那么清楚,因为标准提到了限制交错,而不是预防,它要求数据竞争.
我倾向于保证.该标准提到限制交错,因为仍然允许发生一些不改变结果的交错; 例如, fwrite
一些字节,fseek
返回更多,fwrite
直到原始偏移,以便两个fwrite
s背对背.该实现可以自由地重新排序这些2 fwrite
并将它们合并为一个写入.
1:请参阅R ..的答案中的删除线文字.
它是线程安全的;printf应该是可重入的,并且不会在程序中引起任何奇怪或损坏。
您不能保证一个线程的输出不会从另一个线程的输出开始。如果您担心这一点,则需要开发自己的锁定输出代码以防止多重访问。