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 注解

包裹别名未向 tslint 注册

在 Python 中对 subprocess.PIPE 进行非阻塞读取

在 Python 中对 subprocess.PIPE 进行非阻塞读取

在未显示前缀的包裹上发布 css autoprefixer 问题

包裹在div中时,flex内部的SVG未对齐