CancellationToken请求取消时,NamedPipeServerStream.ReadAsync()不退出

时间:2018-10-03 17:18:18

标签: c# async-await named-pipes cancellationtokensource

NamedPipeServer流从管道读取任何数据时,它不会对CancellationTokenSource.Cancel()做出反应

那是为什么?

如何限制我在服务器中等待来自客户端的数据的时间?

要复制的代码:

static void Main(string[] args)
{
    Server();
    Clinet();
    Console.WriteLine("press [enter] to exit");
    Console.ReadLine();
}

private static async Task Server()
{
    using (var cancellationTokenSource = new CancellationTokenSource(1000))
    using (var server = new NamedPipeServerStream("test",
        PipeDirection.InOut,
        1,
        PipeTransmissionMode.Byte,
        PipeOptions.Asynchronous))
    {
        var cancellationToken = cancellationTokenSource.Token;
        await server.WaitForConnectionAsync(cancellationToken);
        await server.WriteAsync(new byte[]{1,2,3,4}, 0, 4, cancellationToken);
        var buffer = new byte[4];
        await server.ReadAsync(buffer, 0, 4, cancellationToken);
        Console.WriteLine("exit server");
    }
}

private static async Task Clinet()
{
    using (var client = new NamedPipeClientStream(".", "test", PipeDirection.InOut, PipeOptions.Asynchronous))
    {
        var buffer = new byte[4];
        client.Connect();
        client.Read(buffer, 0, 4);
        await Task.Delay(5000);
        await client.WriteAsync(new byte[] {1, 2, 3, 4}, 0, 4);
        Console.WriteLine("client exit");
    }
}

预期结果:

exit server
<client throws exception cuz server closed pipe>

实际结果:

client exit
exit server

编辑

使用CancelIo的答案似乎很有希望,并且确实允许服务器在取消令牌被取消时结束通信。 但是,我不明白为什么在使用ReadPipeAsync时为什么我的“基本方案”无法正常工作。

这是代码,它包含2个客户端功能:

  1. Clinet_ShouldWorkFine-一个能及时读写的优秀客户端
  2. Clinet_ServerShouldEndCommunication_CuzClientIsSlow-客户端速度太慢,服务器应终止通信

预期:

  1. Clinet_ShouldWorkFine-执行结束而没有任何例外
  2. Clinet_ServerShouldEndCommunication_CuzClientIsSlow-服务器关闭管道,客户端抛出异常

实际:

  1. Clinet_ShouldWorkFine-服务器在第一次调用ReadPipeAsync时停止,在1秒后管道关闭,客户端抛出异常
  2. Clinet_ServerShouldEndCommunication_CuzClientIsSlow-服务器关闭管道,客户端抛出异常

服务器使用Clinet_ShouldWorkFine时为什么ReadPipeAsync不起作用

class Program
{
    static void Main(string[] args) {
        // in this case server should close the pipe cuz client is too slow
        try {
            var tasks = new Task[3];
            tasks[0] = Server();
            tasks[1] = tasks[0].ContinueWith(c => {
                Console.WriteLine($"Server exited, cancelled={c.IsCanceled}");
            });
            tasks[2] = Clinet_ServerShouldEndCommunication_CuzClientIsSlow();
            Task.WhenAll(tasks).Wait();
        }
        catch (Exception ex) {
            Console.WriteLine(ex);
        }

        // in this case server should exchange data with client fine
        try {
            var tasks = new Task[3];
            tasks[0] = Server();
            tasks[1] = tasks[0].ContinueWith(c => {
                Console.WriteLine($"Server exited, cancelled={c.IsCanceled}");
            });
            tasks[2] = Clinet_ShouldWorkFine();
            Task.WhenAll(tasks).Wait();
        }
        catch (Exception ex) {
            Console.WriteLine(ex);
        }

        Console.WriteLine("press [enter] to exit");
        Console.ReadLine();
    }

    private static async Task Server()
    {
        using (var cancellationTokenSource = new CancellationTokenSource(1000))
        using (var server = new NamedPipeServerStream("test",
            PipeDirection.InOut,
            1,
            PipeTransmissionMode.Byte,
            PipeOptions.Asynchronous))
        {
            var cancellationToken = cancellationTokenSource.Token;
            await server.WaitForConnectionAsync(cancellationToken);
            await server.WriteAsync(new byte[]{1,2,3,4}, 0, 4, cancellationToken);
            await server.WriteAsync(new byte[]{1,2,3,4}, 0, 4, cancellationToken);
            var buffer = new byte[4];
            var bytes = await server.ReadPipeAsync(buffer, 0, 4, cancellationToken);
            var bytes2 = await server.ReadPipeAsync(buffer, 0, 4, cancellationToken);
            Console.WriteLine("exit server");
        }
    }

    private static async Task Clinet_ShouldWorkFine()
    {
        using (var client = new NamedPipeClientStream(".", "test", PipeDirection.InOut, PipeOptions.Asynchronous))
        {
            var buffer = new byte[4];
            client.Connect();
            client.Read(buffer, 0, 4);
            client.Read(buffer, 0, 4);
            await client.WriteAsync(new byte[] {1, 2, 3, 4}, 0, 4);
            await client.WriteAsync(new byte[] {1, 2, 3, 4}, 0, 4);
            Console.WriteLine("client exit");
        }
    }

    private static async Task Clinet_ServerShouldEndCommunication_CuzClientIsSlow()
    {
        using (var client = new NamedPipeClientStream(".", "test", PipeDirection.InOut, PipeOptions.Asynchronous))
        {
            var buffer = new byte[4];
            client.Connect();
            client.Read(buffer, 0, 4);
            client.Read(buffer, 0, 4);
            await Task.Delay(5000);
            await client.WriteAsync(new byte[] {1, 2, 3, 4}, 0, 4);
            await client.WriteAsync(new byte[] {1, 2, 3, 4}, 0, 4);
            Console.WriteLine("client exit");
        }
    }
}

public static class AsyncPipeFixer {

    public static Task<int> ReadPipeAsync(this PipeStream pipe, byte[] buffer, int offset, int count, CancellationToken cancellationToken) {
        if (cancellationToken.IsCancellationRequested) return Task.FromCanceled<int>(cancellationToken);
        var registration = cancellationToken.Register(() => CancelPipeIo(pipe));
        var async = pipe.BeginRead(buffer, offset, count, null, null);
        return new Task<int>(() => {
            try { return pipe.EndRead(async); }
            finally { registration.Dispose(); }
        }, cancellationToken);
    }

    private static void CancelPipeIo(PipeStream pipe) {
        // Note: no PipeStream.IsDisposed, we'll have to swallow
        try {
            CancelIo(pipe.SafePipeHandle);
        }
        catch (ObjectDisposedException) { }
    }
    [DllImport("kernel32.dll")]
    private static extern bool CancelIo(SafePipeHandle handle);

}

4 个答案:

答案 0 :(得分:9)

.NET程序员编写这样的小测试程序时,在异步/等待方面遇到了可怕的麻烦。它组成不佳,一直都是乌龟。该程序缺少最后的乌龟,任务陷入僵局。没有人愿意让任务继续执行,就像在GUI应用程序中通常会发生的那样。同样非常难以调试。

首先进行较小的更改,以使死锁完全可见:

   int bytes = await server.ReadPipeAsync(buffer, 0, 4, cancellationTokenSource.Token);

这可以避免一些麻烦的事,Server方法使它一直到达“ Server exited”消息。 Task类的一个长期问题是,当任务完成或等待的方法同步完成时,它将尝试直接运行延续。这恰好在该程序中起作用。通过强迫它获得异步结果,僵局现在很明显。


下一步是修复Main(),以使这些任务不再死锁。看起来可能像这样:

static void Main(string[] args) {
    try {
        var tasks = new Task[3];
        tasks[0] = Server();
        tasks[1] = tasks[0].ContinueWith(c => {
            Console.WriteLine($"Server exited, cancelled={c.IsCanceled}");
        });
        tasks[2] = Clinet();
        Task.WhenAll(tasks).Wait();
    }
    catch (Exception ex) {
        Console.WriteLine(ex);
    }
    Console.WriteLine("press [enter] to exit");
    Console.ReadLine();
}

现在我们有办法取得进展,并实际解决取消问题。 NamedPipeServerStream类本身并不实现ReadAsync,它从其基类之一Stream继承了该方法。它有一个很少的细节,完全没有记录在案。您只有凝视framework source code时才能看到它。它只能在调用ReadAsync()之前 发生取消时检测到取消。一旦开始读取,它将不再看到取消。您要解决的最终问题。

这是一个可解决的问题,我只有一个模糊的主意,为什么Microsoft不对PipeStreams执行此操作。强制BeginRead()方法提早完成的通常方法是Dispose()对象,这也是可以中断Stream.ReadAsync()的唯一方法。但是还有另一种方法,在Windows上可以使用CancelIo()中断I / O操作。让我们将其作为扩展方法:

using System;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.IO.Pipes;
using Microsoft.Win32.SafeHandles;

public static class AsyncPipeFixer {

    public static Task<int> ReadPipeAsync(this PipeStream pipe, byte[] buffer, int offset, int count, CancellationToken cancellationToken) {
        if (cancellationToken.IsCancellationRequested) return Task.FromCanceled<int>(cancellationToken);
        var registration = cancellationToken.Register(() => CancelPipeIo(pipe));
        var async = pipe.BeginRead(buffer, offset, count, null, null);
        return new Task<int>(() => {
            try { return pipe.EndRead(async); }
            finally { registration.Dispose(); }
        }, cancellationToken);
    }

    private static void CancelPipeIo(PipeStream pipe) {
        // Note: no PipeStream.IsDisposed, we'll have to swallow
        try {
            CancelIo(pipe.SafePipeHandle);
        }
        catch (ObjectDisposedException) { }
    }
    [DllImport("kernel32.dll")]
    private static extern bool CancelIo(SafePipeHandle handle);

}

最后调整服务器以使用它:

    int bytes = await server.ReadPipeAsync(buffer, 0, 4, cancellationTokenSource.Token);

请注意,此解决方法仅适用于Windows,因此不能在针对Unix风格的.NETCore程序中使用。然后考虑重锤,在CancelPipeIo()方法中调用pipe.Close()。

答案 1 :(得分:0)

ReadAsync首先检查取消,然后开始阅读是否已取消的令牌无效

添加以下行

  

cancellationToken.Register(server.Disconnect);

using (var cancellationTokenSource = new CancellationTokenSource(1000))
using (var server = new NamedPipeServerStream("test",
    PipeDirection.InOut,
    1,
    PipeTransmissionMode.Byte,
    PipeOptions.Asynchronous))
{
    var cancellationToken = cancellationTokenSource.Token;
    cancellationToken.Register(server.Disconnect);
    await server.WaitForConnectionAsync(cancellationToken);
    await server.WriteAsync(new byte[]{1,2,3,4}, 0, 4, cancellationToken);
    var buffer = new byte[4];
    await server.ReadAsync(buffer, 0, 4, cancellationToken);
    Console.WriteLine("exit server");
}

答案 2 :(得分:0)

我只是在看您的代码,也许是在看一眼...

据我所知,在您的原始场景以及随后的更复杂场景中……您正在传递一个已经取消的取消令牌,这几乎是无法预测的,其他人如何实现方法中抛出的异常(如果有)。

  

使用IsCancellationRequested属性检查令牌是否已被取消,并且不传递已取消的令牌。

以下是将其添加到原始问题的代码中的示例(您可以为以后的ReadPipeAsync方法执行相同操作。

var cancellationToken = cancellationTokenSource.Token;
await server.WaitForConnectionAsync(cancellationToken);

if(!cancellationToken.IsCancellationRequested)
{
    await server.WriteAsync(new byte[] { 1, 2, 3, 4 }, 0, 4, cancellationToken);
}

if(!cancellationToken.IsCancellationRequested)
{
    var buffer = new byte[4];
    await server.ReadAsync(buffer, 0, 4, cancellationToken);
}

Console.WriteLine("exit server");

上面的代码将导致

exit server
client exit

我认为这也是您最原始的问题...

答案 3 :(得分:0)

Hans Passant 的回答是理想的……几乎。唯一的问题是 CancelIo() 取消了从同一线程完成的请求。如果任务在不同的线程上恢复,这将不起作用。不幸的是,我没有足够的声望点直接评论他的答案,因此单独回答。

所以他的示例代码的最后一部分应该重写如下:

    private static void CancelPipeIo(PipeStream pipe) {
        // Note: no PipeStream.IsDisposed, we'll have to swallow
        try {
            CancelIoEx(pipe.SafePipeHandle);
        }
        catch (ObjectDisposedException) { }
    }
    [DllImport("kernel32.dll")]
    private static extern bool CancelIoEx(SafePipeHandle handle, IntPtr _ = default);

请注意,CancelIoEx() 在 Vista/Server 2008 及更高版本中可用,而 CancelIo() 在 Windows XP 中也可用。