StreamWriter 在其包裹的 Pipe 未排空时无法关闭?
Posted
技术标签:
【中文标题】StreamWriter 在其包裹的 Pipe 未排空时无法关闭?【英文标题】:StreamWriter can't be closed while its wrapped Pipe isn't drained? 【发布时间】:2016-09-25 14:54:27 【问题描述】:我有一个使用命名管道的简单服务器-客户端应用程序。我在服务器中使用 StreamWriter,在客户端中使用 StreamReader。只要客户端进程不从管道读取(即不从 StreamReader 读取,包装管道),StreamWriter 就不会被释放。我想知道为什么。
详情如下:
这是服务器:
using System;
using System.IO;
using System.IO.Pipes;
class PipeServer
static void Main()
using (NamedPipeServerStream pipeServer =
new NamedPipeServerStream("testpipe"))
Console.Write("Waiting for client connection...");
pipeServer.WaitForConnection();
Console.WriteLine("Client connected.");
try
StreamWriter sw = new StreamWriter(pipeServer);
try
sw.WriteLine("hello client!");
finally
sw.Dispose();
// Would print only after the client finished sleeping
// and reading from its StreamReader
Console.WriteLine("StreamWriter is now closed");
catch (IOException e)
Console.WriteLine("ERROR: 0", e.Message);
这是客户:
using System;
using System.IO;
using System.IO.Pipes;
using System.Threading;
class PipeClient
static void Main(string[] args)
using (NamedPipeClientStream pipeClient =
new NamedPipeClientStream(".", "testpipe"))
Console.Write("Attempting to connect to pipe...");
pipeClient.Connect();
Console.WriteLine("Connected to pipe.");
using (StreamReader sr = new StreamReader(pipeClient))
Thread.Sleep(100000);
string temp = sr.ReadLine();
if (temp != null)
Console.WriteLine("Received from server: 0", temp);
注意客户端中的Thread.Sleep(100000);
:我添加它是为了确保只要客户端进程处于休眠状态,StreamWriter 就不会被释放到服务器中,并且服务器不会执行Console.WriteLine("StreamWriter is now closed");
。为什么?
编辑:
我切断了之前的信息,我认为这些信息可能无关紧要。 我还想补充一点——多亏了 cmets 中的 Scott——我观察到这种行为发生了相反的情况:如果服务器写入,然后休眠,客户端(尝试)使用它的 StreamReader 读取——读取是'在服务器唤醒之前不会发生。
第二次编辑:
我在第一次编辑中谈到的其他方式的行为无关紧要,这是一个flush
问题。
我试着给它更多的试验,并得出结论斯科特是对的——如果不排干管道就不能处理。那为什么?这似乎也与 StreamWriter
假定它拥有流的事实相矛盾,除非另有说明(参见 here)。
以下是上面代码中添加的细节:
在服务器程序中,try-finally
现在看起来像这样:
try
sw.AutoFlush = true;
sw.WriteLine("hello client!");
Thread.Sleep(10000);
sw.WriteLine("hello again, client!");
finally
sw.Dispose(); // awaits while client is sleeping
Console.WriteLine("StreamWriter is now closed");
在客户端程序中,using
块现在如下所示:
using (StreamReader sr = new StreamReader(pipeClient))
string temp = sr.ReadLine();
Console.WriteLine("blah"); // prints while server sleeps
Console.WriteLine("Received from server: 0", temp); // prints while server is sleeping
Thread.Sleep(10000);
temp = sr.ReadLine();
Console.WriteLine("Received from server: 0", temp);
【问题讨论】:
如果将服务器更改为new NamedPipeServerStream("testpipe", PipeDirection.Out)
,将客户端更改为new NamedPipeClientStream(".", "testpipe", PipeDirection.In)
,行为是否会发生变化?另外,如果你在读取之后放置了一个Thread.Sleep(100000);
,但仍然在客户端的using
内,会发生什么情况,流在退出使用之前是否仍然在服务器上保持打开状态?
@ScottChamberlain,完全没有变化,而改变Sleep
的位置是我所期望的:服务器进程继续,打印"StreamWriter is now closed"
并返回,而客户端休眠。跨度>
也许在缓冲区耗尽之前它不会让你关闭。如果你这样做Server write -> Client read -> Server write -> Server dispose -> Client read
会发生什么?
您的期望是什么?它工作得很好,不是吗?我确实发现了另一件事:我尝试了Server write -> Server long sleeping -> Client read
- 但客户端直到服务器醒来才读取。
我认为它会一直挂到客户端的第二次读取,这表明它会阻止 dispose 直到缓冲区为空。老实说,你不是我的专业知识,我不知道是什么原因造成的,也不知道如何预防。
【参考方案1】:
所以问题在于 Windows 命名管道的工作方式。当CreateNamedPipe 被调用时,您可以指定输出缓冲区大小。现在文档是这样说的:
输入和输出缓冲区大小是建议性的。为命名管道的每一端保留的实际缓冲区大小是系统默认值、系统最小值或最大值,或者是向上舍入到下一个分配边界的指定大小。
这里的问题是 NamedPipeServerStream 构造函数将 0 作为默认输出大小传递(我们可以使用源代码验证这一点,或者在我的情况下只是启动 ILSpy)。根据注释,您可能会假设这将创建一个“默认”缓冲区大小,但事实并非如此,它实际上创建了一个 0 字节的输出缓冲区。这意味着除非有人正在读取缓冲区,否则对管道的任何写入都会阻塞。您还可以使用OutBufferSize 属性验证大小。
您看到的行为是因为当您处理 StreamWriter 时,它会在管道流上调用 Write(因为它已经缓冲了内容)。 write 调用阻塞,因此 Dispose 永远不会返回。
为了验证这一点,我们将使用 WinDBG(因为 VS 仅在 sw.Dispose 调用中显示阻塞线程)。
在 WinDBG 下运行服务器应用程序。当它阻塞时暂停调试器(例如按 CTRL+Break)并发出以下命令:
加载 SOS 调试器模块:.loadby sos clr
使用交错的托管和非托管堆栈帧转储所有正在运行的堆栈(由于 sos.dll 中的愚蠢错误,可能需要运行两次):!eestack
您应该看到该过程中的第一个堆栈看起来像这样(为简洁起见进行了大量编辑):
Thread 0
Current frame: ntdll!NtWriteFile+0x14
KERNELBASE!WriteFile+0x76, calling ntdll!NtWriteFile
System.IO.Pipes.PipeStream.WriteCore
System.IO.StreamWriter.Flush(Boolean, Boolean))
System.IO.StreamWriter.Dispose(Boolean))
所以我们可以看到我们被卡在了 NtWriteFile 中,这是因为它无法将字符串写入大小为 0 的缓冲区。
要解决此问题很容易,请使用 NamedPipeServerStream 构造函数之一指定显式输出缓冲区大小,例如 this 之一。例如,这将执行默认构造函数将执行的所有操作,但输出缓冲区大小合理:
new NamedPipeServerStream("testpipe", PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.None, 0, 4096)
【讨论】:
我可能在您的解释中遗漏了一些内容,但是:为什么 write 调用仅在 Dispose 上阻塞?您说它与零大小缓冲区有连接。那么,为什么任何其他的 write 调用都不会阻塞呢?我不明白的另一件事是为什么服务器摆脱其在 Dispose 中的阻塞会受到客户端摆脱睡眠的影响?顺便说一句,我必须掌握 WinDBG 的窍门,因为我还不太熟悉它。与此同时,我很乐意提供更多说明。 因此阻塞只发生在 Dispose 中,因为 StreamWriter 会缓冲内容直到达到一定数量,然后再将其写入底层文件句柄(最终是命名管道对操作系统而言)和除非它达到缓冲区限制,否则它不会调用 Write。如果您直接调用 Stream::Write,它 应该 阻塞,但您当然不会,您将它包装在 StreamWriter 中。 至于为什么在客户端唤醒并调用ReadLine后退出,那是因为它从管道中读取数据,这解除了它退出的Dispose调用的隐式Write,每个人都很高兴。 我现在更明白了。不过还有一个:“StreamWriter 缓冲”-我认为AutoFlush = true
阻止了这一点:我查看了StreamWriter.Write(string value)
,最后那里有一个对Flush
的调用-在Flush
内确实有一个调用到stream.Write
。因此,如果我理解正确 - 如果它阻止 Dispose
,它应该阻止 sw.WriteLine
,但它没有。
好吧,我是基于您的原始实现,如果我添加 AutoFlush = true
然后它会在 WriteLine 调用中阻塞。同样,您可以使用 WinDBG 轻松验证这一点 :-)以上是关于StreamWriter 在其包裹的 Pipe 未排空时无法关闭?的主要内容,如果未能解决你的问题,请参考以下文章
未捕获的错误:模块“AppModule”声明的意外模块“FormsModule”。请添加@Pipe/@Directive/@Component 注解
在 Python 中对 subprocess.PIPE 进行非阻塞读取
在 Python 中对 subprocess.PIPE 进行非阻塞读取