为什么printf在调用后没有刷新,除非格式字符串中有换行符?

692
为什么printf在调用后没有换行符的情况下不自动刷新输出缓冲区?这是否符合POSIX标准?如何让printf每次都立即刷新缓冲区?

2
你调查过这是否发生在任何文件上还是只发生在终端上吗?这听起来像是一个聪明的终端功能,不会从后台程序输出未完成的行,尽管我希望它不适用于前台程序。 - PypeBros
8
在Cygwin bash下,即使在格式字符串中有换行符,我仍然遇到了相同的问题。这个问题出现在Windows 7上,而相同的源代码在Windows XP上运行良好。MS cmd.exe可以如预期地刷新缓存。修复方法 setvbuf(stdout, (char*)NULL, _IONBF, 0) 可以解决这个问题,但这显然不应该是必须的。我正在使用MSVC++ 2008 Express。 - Steve Pitchers
17
为了澄清问题标题,printf(..)本身不会执行任何刷新操作,它是stdout缓冲区在看到换行符时可能会刷新的结果(如果是行缓冲)。对于putchar('\n'),它将以同样的方式作出反应,因此在这方面printf(..)并不特别。这与cout << endl;不同,其文档明确提到了刷新。printf的文档并没有提到刷新。 - Evgeni Sergeev
2
写入/刷新(flush)是一种可能会消耗大量资源的操作,因此它通常会有缓冲以提高性能。 - hanshenrik
@EvgeniSergeev:是否有共识认为该问题错误地诊断了问题,并且只有在输出中有换行符时才会发生刷新?(在格式字符串中放置一个换行符是一种方法,但不是获取输出中的换行符的唯一方法)。 - Ben Voigt
10个回答

906

默认情况下,stdout流是按行缓冲的,所以只有在遇到换行符(或被告知时)后才会显示缓冲区中的内容。你有几个选项可以立即打印:

  • 使用fprintf将输出打印到stderr而不是stdoutstderr默认情况下是不带缓冲区的):

fprintf(stderr, "I will be printed immediately");
  • 使用fflush 在需要时刷新stdout

    printf("Buffered, will be flushed");
    fflush(stdout); // Will now print everything in the stdout buffer
    
  • 使用setbuf禁用标准输出的缓冲:

  • setbuf(stdout, NULL);
    
  • 或者使用更灵活的setvbuf函数:

  • setvbuf(stdout, NULL, _IONBF, 0); 
    

    290
    或者,要完全禁用缓冲:setbuf(stdout, NULL); - Andy Ross
    108
    另外,只想提一下,在UNIX中,如果标准输出(stdout)不是终端,那么换行符通常不会刷新缓冲区。如果输出被重定向到文件,则换行符不会使其刷新。 - hora
    9
    我感觉应该补充一下:我刚刚在测试这个理论,发现在非终端流上使用setlinebuf()会在每行末尾进行刷新。 - Doddy
    8
    初始时,标准错误流并未完全缓冲;只有在确定流不是指向交互设备时,标准输入和标准输出流才被完全缓冲。请参考这个问题:https://dev59.com/nW435IYBdhLWcg3wxC9p - Seppo Enarvi
    3
    如果这篇文章要成为一个好的关于“为什么它没有打印”的标准回答,那么根据"Does printf always flush the buffer on encountering a newline?"中明确提到的终端/文件区别,直接在这个广受赞同的答案中提到似乎很重要,而不是让人们需要阅读评论。 - HostileFork says dont trust SE
    显示剩余10条评论

    152

    不,这不是POSIX行为,而是ISO C行为(实际上,只有在符合ISO C时才是POSIX行为)。

    如果可以检测到标准输出是交互设备,则其为行缓冲,否则为完全缓冲。因此,在某些情况下,即使printf获得了要发送的换行符,它也不会刷新,例如:

    myprog >myfile.txt
    

    这种做法在效率上是有道理的,因为如果你与用户交互,他们可能希望看到每一行。如果将输出发送到文件,则很可能在另一端没有用户(虽然不是不可能,他们可能正在关注该文件)。现在你可以争辩说用户想要看到每个字符,但存在两个问题。

    第一,这不是很高效。 第二,原始的 ANSI C 89 要求主要是为了编码现有行为,而不是发明行为,并且那些设计决策是在 ANSI 开始进程之前做出的。即使现在的 ISO C 在更改标准中的现有规则时也非常小心谨慎。

    至于如何处理这个问题,如果你在每个要立即查看的输出调用之后使用fflush(stdout),那么问题就解决了。

    或者,您可以在操作stdout之前使用setvbuf,将其设置为无缓存,这样您就不必担心将所有这些fflush行添加到代码中:

    setvbuf (stdout, NULL, _IONBF, BUFSIZ);
    

    请记住,如果您将输出发送到文件,则可能会严重影响性能。还要记住,对此的支持是实现定义的,而不是标准保证。

    ISO C99第7.19.3/3节是相关部分:

    当流是“无缓冲”的时候,字符应该尽快从源或目的地出现。否则,字符可能会被积累并作为块传输到或从主机环境中传输。
    当流是“完全缓冲”的时候,字符应该在缓冲区填满时作为块传输到或从主机环境中传输。
    当流是“行缓冲”的时候,字符应该在遇到换行符时作为块传输到或从主机环境中传输。
    此外,当缓冲区填满时,请求无缓冲流上的输入或需要从主机环境传输字符的行缓冲流上的输入时,字符应作为块传输到主机环境。
    对这些特性的支持是实现定义的,并且可以通过setbuf和setvbuf函数进行影响。

    11
    我刚遇到一个情况,即使有 '\n',printf() 也不会刷新缓冲区。根据你在这里提到的方法,可以通过添加 fflush(stdout) 来解决。但我想知道为什么 '\n' 在 printf() 中不能刷新缓冲区。 - Qiang Xu
    11
    @QiangXu,标准输出仅在可以明确确定它指向交互设备的情况下才是行缓冲。例如,如果您使用myprog >/tmp/tmpfile重定向输出,则它是完全缓冲而不是行缓冲。我记得,确定您的标准输出是否为交互式是由实现来决定的。 - paxdiablo
    4
    此外,在Windows系统中,调用setvbuf(...., _IOLBF)将不起作用,因为在该系统中_IOLBF与_IOFBF相同:https://msdn.microsoft.com/en-us/library/86cebhfs.aspx - Piotr Lopusiewicz

    38

    这可能是为了效率考虑,同时如果有多个程序向单个TTY写入,这样做可以避免行中的字符相互交错。因此,如果程序A和B都输出,通常会得到:

    program A output
    program B output
    program B output
    program A output
    program B output
    

    虽然这很糟糕,但比起其他选择还是好的。

    proprogrgraam m AB  ououtputputt
    prproogrgram amB A  ououtputtput
    program B output
    
    请注意,甚至不能保证在换行时刷新缓冲区,因此如果刷新对您很重要,则应明确进行刷新。

    有趣的是,我认为这是唯一一个真正回答了“为什么?”的答案——适当地进行猜测。其他人解释了*它是如何缓存的(这似乎对于一个使用术语“刷新”表现出意识的OP来说不太必要),以及如何避免/控制它。不可否认,在这些回答中有足够的细节提供有益的见解。但只有这一个讨论了为什么,并且拥有自己回答所有问题的角度。干杯。 - Yunnosch
    请注意这个回答中的"usually"——在许多场景(颜色、长行、stderr)中,输出会是交错的。 - julaine

    37
    为了立即刷新,请调用fflush(stdout)fflush(NULL)NULL表示刷新所有内容)。

    45
    记住,fflush(NULL);通常是一个非常糟糕的想法。如果您打开了许多文件,特别是在多线程环境中,将与所有内容竞争锁定,这将影响性能。 - R.. GitHub STOP HELPING ICE

    15

    stdout是有缓冲的,只有在打印换行符后才会输出。

    要立即输出,可以:

    1. 将内容打印到stderr。
    2. 使stdout无缓冲。

    16
    或者 fflush(stdout) - RastaJedi
    5
    “so will only output after a newline is printed.” 的意思是“只有在打印换行符后才会输出”。除此之外,还有至少四种情况:缓冲区已满、写入到 stderr(本回答稍后提到)、fflush(stdout)fflush(NULL) - chux - Reinstate Monica
    1
    “stdout is buffered”这句话并不准确,正如第二个要点所暗示的那样。默认情况下,当stdout是一个普通文件时,它是块缓冲的;当它是tty时,它是行缓冲的。也许只需在短语“stdout is buffered”中添加“默认情况下”即可。 - William Pursell

    15

    3
    在“正常”情况下,比直接将内容打印到终端更糟糕的是,即使在立即使用其输出的情况下,printffprintf 的缓冲也会更加粗略。除非微软已经解决了这个问题,否则这将使得一个程序无法捕获另一个程序的标准错误和标准输出,并确定发送到每个流的顺序。 - supercat
    不,除非没有设置缓冲区,否则它不会立即打印到终端。默认情况下使用完全缓冲。 - phuclv

    11

    默认情况下,标准输出是行缓冲的,标准错误输出是无缓冲的,文件则是完全缓冲的。


    10
    您可以输出到未缓冲的 stderr。或者在需要时刷新 stdout。或者将 stdout 设置为未缓冲。

    10
    使用setbuf(stdout, NULL);来禁用缓冲。

    10

    一般有两种缓冲级别 -

    1. 内核缓存(使读/写更快)

    2. I/O库中的缓冲(减少系统调用次数)

    让我们以fprintf和write()为例。

    当您调用fprintf()时,它不会直接写入文件。它首先进入程序内存中的stdio缓冲区。从那里通过使用写入系统调用将其写入内核缓存。因此,跳过I/O缓冲区的一种方法是直接使用write()。其他方法是使用setbuff(stream,NULL)。这将缓冲模式设置为无缓冲,并且数据直接写入内核缓冲区。 为了强制数据转移到内核缓冲区,我们可以使用“\n”,在默认的“行缓冲”缓冲模式下,将刷新I/O缓冲区。 或者我们可以使用fflush(FILE * stream)

    现在我们在内核缓冲区中。内核(/操作系统)想要最小化磁盘访问时间,因此只读取/写入磁盘块。因此,当发出read()(一种系统调用,可以直接调用或通过fscanf()调用)时,内核从磁盘中读取磁盘块并将其存储在缓冲区中。之后将数据从这里复制到用户空间。

    类似地,fprintf()从I/O缓冲区接收的数据被内核写入磁盘。这使得read() write()更快。

    现在为了强制内核启动write(),之后数据传输由硬件控制器控制,还有一些方法。我们可以在写入调用期间使用O_SYNC或类似标志。或者我们可以使用其他函数如fsync(),fdatasync(),sync(),使内核在数据可用于内核缓冲区时立即启动写入。


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