11

When the NamedPipeServer stream reads any data from the pipe it does not react to CancellationTokenSource.Cancel()

Why is that?

How can I limit the time I'm waiting in the server for data from the client?

Code to reproduce:

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");
    }
}

Expected result:

exit server
<client throws exception cuz server closed pipe>

Actual result:

client exit
exit server

EDIT

The answer with CancelIo seems promising, and it does allow the server to end communication when the cancellation token is canceled. However, I don't understand why my "base scenario" stopped working when using ReadPipeAsync.

Here is the code, it includes 2 client functions:

  1. Clinet_ShouldWorkFine - a good client that reads/writes in time
  2. Clinet_ServerShouldEndCommunication_CuzClientIsSlow - a client too slow, server should end the communication

Expected:

  1. Clinet_ShouldWorkFine - execution ends without any excepiton
  2. Clinet_ServerShouldEndCommunication_CuzClientIsSlow - server closes the pipe, client throws exception

Actual:

  1. Clinet_ShouldWorkFine - server stops at first call to ReadPipeAsync, pipe is closed afer 1s, client throws exception
  2. Clinet_ServerShouldEndCommunication_CuzClientIsSlow - server closes the pipe, client throws exception

Why is Clinet_ShouldWorkFine not working when the server uses 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);

}
Tony Edgecombe
  • 3,860
  • 3
  • 28
  • 34
inwenis
  • 368
  • 7
  • 24
  • @MrinalKamboj I'm using `new CancellationTokenSource(1000)` which calls `.Cancel()` after the specified time passes - in this case after 1000ms – inwenis Oct 06 '18 at 08:31
  • @MrinalKamboj where should I add the `Task.Delay(1000)`? Sorry I don't get it. A side note: the code above exactly demontrates the really thing where I faced this problem. I run a python script from C# and talk to it via the pipe. I can't just add delays here or there since I know that C# get stuck exactly at the `ReadAsync()`. – inwenis Oct 06 '18 at 08:51

4 Answers4

19

.NET programmers get horribly in trouble with async/await when they write little test programs like this. It composes poorly, it is turtles all the way up. This program is missing the final turtle, the tasks are deadlocking. Nobody is taking care of letting the task continuations execute, as would normally happen in (say) a GUI app. Exceedingly hard to debug as well.

First make a minor change so the deadlock is completely visible:

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

This takes a nasty little corner-case away, the Server method making it all the way to the "Server exited" message. A chronic problem with the Task class is that when a task completes or an awaited method finished synchronously then it will try to run the continuation directly. That happens to work in this program. By forcing it to obtain the async result, the deadlock is now obvious.


Next step is to fix Main() so these tasks can't deadlock anymore. That could look like this:

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();
}

Now we have a shot at getting ahead and actually fix the cancellation problem. The NamedPipeServerStream class does not implement ReadAsync itself, it inherits the method from one of its base classes, Stream. It has a ratty little detail that is completely under-documented. You can only see it when you stare at the framework source code. It can only detect cancellation when the cancel occurred before you call ReadAsync(). Once it the read is started it no longer can see a cancellation. The ultimate problem you are trying to fix.

It is a fixable problem, I have but a murky idea why Microsoft did not do this for PipeStreams. The normal way to force a BeginRead() method to complete early is to Dispose() the object, also the only way that Stream.ReadAsync() can be interrupted. But there is another way, on Windows it is possible to interrupt an I/O operation with CancelIo(). Let's make it an extension method:

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);

}

And finally tweak the server to use it:

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

Do beware that this workaround is specific to Windows so can't work in a .NETCore program that targets a Unix flavor. Then consider the heavier hammer, call pipe.Close() in the CancelPipeIo() method.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • This is really interesting. I now understand why the server was `deaf` to cancellation messages. However I don't know why using the `ReadPipeAsync` does not work in my `Happy path`. Added code to my original post to demo to issue with a "good" client – inwenis Oct 07 '18 at 19:08
  • Please be more clear about what "does not work" and "happy path" mean. As written the client should not work, you should get the "Pipe is broken" exception message. – Hans Passant Oct 08 '18 at 14:06
  • I have added an edit to my origin post. It includes 2 "clients". 1. `Clinet_ServerShouldEndCommunication_CuzClientIsSlow()` is a representation of a slow client, it has a `Task.Delay(5000)`, in this case the server should end communication cuz the client is too slow. 2. `Clinet_ShouldWorkFine()` representa a `HappyPath` ie. a well behaving client, server should be able to exchange data with `Clinet_ShouldWorkFine()` without any errors/exceptions. – inwenis Oct 08 '18 at 15:58
  • Should there be an `AsyncCallback` here instead of `null`? Almost all similar named pipe code I see has the callback in place. Just wondering why it's omitted here? – Michael Parker Mar 08 '19 at 15:20
  • Great suggestion Hans. I've used `CancelIo` in my unmanaged applications, not sure why it didn't occur to me to use it in my managed application. – WBuck Sep 13 '19 at 01:32
  • 1
    I tried this, and as in the OP's revised question above, found it didn't work for me. What is working, is to replace `new Task` in ReadPipeAsync with `Task.Run`. But I don't pretend to know enough about this to know what other chaos I might be causing! – Trygve Feb 05 '20 at 22:02
  • I recommend Nito.AsyncEx. You can create an AsyncContextThread there, in which you can run your async-await app. This will execute all code in a single thread, like in a WPF app. Extremely useful, as you otherwise have to code everything threadsafe, which is very hard to get absolutely right. – AyCe Nov 08 '21 at 15:43
1

The answer from Hans Passant is ideal... almost. The only issuse is that CancelIo() cancels request done from the same thread. This won't work if the task gets resumed on a different thread. Unfortunately, I do not have enough reputation points to comment his answer directly, thus answering separately.

So the last part of his example code should be rewritten as the following:

    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);

Note that the CancelIoEx() is available in Vista/Server 2008 and later, while CancelIo() is also available in Windows XP.

0

ReadAsync First check for cancellation then start reading if the token canceled it doesn't have effect

add following line

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");
}
Milad
  • 379
  • 1
  • 16
  • How does the `Sever.Disconnect` helps here on Cancellation invocation. OP is looking for the Exception, which is not happening – Mrinal Kamboj Oct 06 '18 at 19:33
  • if the server's pipe is closed the Clinet() method will throw exception. as the expected result is. – Milad Oct 06 '18 at 20:13
  • Genuine exception should come from the server, client should not manufacture exception, based on simple event like Disconnect – Mrinal Kamboj Oct 06 '18 at 20:27
  • @Milad, thanks for that. I'm actually currently using `cancellationToken.Register(server.Disconnect);` as a workaround and it does handle the initial case I have posted correctly. However it does not work when the client does not read the data from the pipe. In this case server waits on the `WriteAsync` and when `server.Disconnect` happens `WriteAsync` throws `PipeClosedException` or simillar. – inwenis Oct 07 '18 at 19:16
0

I'm just looking at your code and maybe a fresh pair of eyes on it...

As far as I can tell, in both your original and then further complex scenarios... you are passing an already cancelled cancellation token, which is pretty unpredictable how others implement (if any) exceptions being thrown within the methods...

Use the IsCancellationRequested property to check if the token is already cancelled and don't pass cancelled tokens.

Here is a sample of adding this into your code from the original question (you can do the same for your later ReadPipeAsync method.

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");

the above code will result in

exit server
client exit

which I think was your very original question too...

Svek
  • 12,350
  • 6
  • 38
  • 69