如果我的程序将日志输出到标准输出(stdout),并且以重定向文件或管道到另一个程序的方式调用,如果缓冲区没有及时被操作系统或管道接收方消耗,我的程序会停顿吗?
是的,会停顿。
当你调用Console.SetOut()时,以下代码将被执行:
public static void SetOut(TextWriter newOut) {
if (newOut == null)
throw new ArgumentNullException("newOut");
Contract.EndContractBlock();
#pragma warning disable 618
new SecurityPermission(SecurityPermissionFlag.UnmanagedCode).Demand();
#pragma warning restore 618
#if FEATURE_CODEPAGES_FILE // if no codepages file then we are locked into default codepage and this field is not used
_isOutTextWriterRedirected = true;
#endif
newOut = TextWriter.Synchronized(newOut);
lock(InternalSyncObject) {
_out = newOut;
}
}
即,新的输出
TextWriter
始终是由
TextWriter.Syncronized()
返回的一个写入器。这反过来总是返回一个
SyncTextWriter
对象。如果您查看该类的
WriteLineAsync()
方法,它会执行两个操作:
- 它本身是一个同步方法,这意味着如果多个线程尝试使用该写入器,则实际上只有一个线程会使用它(即所有带有该注释的方法在线程之间是互斥的)。
- 更重要的是,
WriteLineAsync()
方法调用底层TextWriter
上的同步WriteLine()
方法,该方法用于创建SyncTextWriter
对象。
那第二点的意思是该方法在操作完成前不会返回。它总是返回一个已完成的任务。它没有办法返回一个未完成的任务,如果操作因任何原因被阻塞,例如输出流的使用者没有消耗它,从而导致输出缓冲区被填满,调用将不会返回。
这将导致您进行此类调用的线程被阻塞,直到写操作可以完成,即最初导致其阻塞的任何条件都已解决。
现在,所有这些都说完了:
- 如果你正在写入文件,无论是因为调用了
SetOut()
还是因为进程的标准输出已被重定向,这可能会使程序变慢,具体取决于程序生成的输出量相对于其他工作的多少,但我不认为你会遇到任何被称为“停顿”的情况。即磁盘I/O很慢,但通常不会阻塞很长时间。
- 如果调用
SetOut()
并传递其他类型的writer,假设您对其具有控制权,并且可以确保正确编写并在编写时不会阻塞。
- 如果stdout外部被重定向到其他目标,例如启动了您的进程并重定向了
Process.StandardOutput
流的第二个.NET程序,则该程序应确保跟上,如果它关心您的程序能够继续无障碍地进行。但无论如何都是这样的;进程输出流的使用者应始终确保尽快读取它们。
我个人认为程序本身不应该保护自己免受可能会阻塞的重定向流的影响,除非当然不将控制台流内部重定向到可能的目标对象。通常情况下,这被认为是接收进程的工作,因为它是唯一关心重定向程序是否能够完成其工作的进程。
如果真的有问题,你可以在进程内部缓冲输出,例如使用一个
BlockingCollection<string>
对象来协调想要写入输出的线程和实际执行写入操作的消费线程。但这并不是很有用,因为这只是把问题推迟了,如果不能指望消费线程能够不受阻碍地继续进行,你不希望无限期地写入集合。如果消费者真的被阻塞,集合将变得太大,并在某些时候抛出
OutOfMemoryException
。您将遇到与任何其他机制类似的问题,该机制旨在将写入委托给不同的线程,以便让当前线程在不受阻塞的情况下运行。
在我看来,最好只是正常地编写代码(即,甚至不要使用
...Async
方法,除非您的代码将编写抽象为
TextWriter
并且它旨在在支持异步时异步工作),并依靠流的消费端“做正确的事情”,而不会阻塞您的进程。