当包裹的管道没有耗尽时,StreamWriter无法关闭?

时间:2016-09-25 14:54:27

标签: c# pipe

我有一个简单的服务器 - 客户端应用程序,使用命名管道。我在服务器中使用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就不会在服务器中处理,并且服务器将不会执行{{1} }。为什么?

修改

我切断了以前的信息,我认为这可能是无关紧要的。 我还想补充一点 - 感谢Scott在评论中 - 我发现这种行为发生了反过来:如果服务器写入,然后睡眠,客户端(尝试)使用其StreamReader读取 - 读取不是'发生,直到服务器醒来。

第二次编辑:

我在第一次编辑中谈到的其他方式是无关紧要的,这是一个Console.WriteLine("StreamWriter is now closed");问题。 我试着给它做一些试验,并得出结论斯科特的权利 - 如果没有排水管道就不能处理。那为什么?这似乎与flush假定它拥有流的事实相矛盾,除非另有说明(见here)。

以下是上述代码的附加细节:

在服务器程序中,StreamWriter现在看起来像这样:

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

1 个答案:

答案 0 :(得分:3)

所以问题在于Windows命名管道的工作方式。调用CreateNamedPipe时,您可以指定输出缓冲区大小。现在文档说的是这样的:

  

输入和输出缓冲区大小是建议性的。为命名管道的每一端保留的实际缓冲区大小是系统默认值,系统最小值或最大值,或指定的大小向上舍入到下一个分配边界。

这里的问题是NamedPipeServerStream构造函数传递0作为默认输出大小(我们可以使用源验证这一点,或者在我的情况下只是启动ILSpy)。您可能会认为这会根据注释创建一个“默认”缓冲区大小,但事实并非如此,它实际上会创建一个0字节的输出缓冲区。这意味着除非有人正在读取缓冲区,否则对管道块进行任何写入。您还可以使用OutBufferSize属性验证大小。

您看到的行为是因为当您处理StreamWriter时,它会在管道流上调用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)