Skip to content

Instantly share code, notes, and snippets.

@AArnott
Last active March 9, 2023 11:10
Show Gist options
  • Save AArnott/0d5f4645ad7e9a765cee to your computer and use it in GitHub Desktop.
Save AArnott/0d5f4645ad7e9a765cee to your computer and use it in GitHub Desktop.
Async named pipes example
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>exe</OutputType>
<TargetFrameworks>net472;net5.0-windows</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.IO.Pipes.AccessControl" Version="5.0.0" />
</ItemGroup>
</Project>
using System;
using System.IO;
using System.IO.Pipes;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
const string pipeName = @"testpipe";
Task serverTask = RunServerAsync(pipeName);
Task clientTask = RunClientAsync(pipeName);
Task.WaitAll(serverTask, clientTask);
}
private static async Task RunServerAsync(string pipeName)
{
PipeSecurity security = new PipeSecurity();
security.AddAccessRule(new PipeAccessRule($"{Environment.UserDomainName}\\{Environment.UserName}", PipeAccessRights.ReadWrite, System.Security.AccessControl.AccessControlType.Allow));
#if NET5_0 // .NET Core 3.1 does not support setting ACLs on named pipes.
NamedPipeServerStream serverPipe = NamedPipeServerStreamAcl.Create(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Message, PipeOptions.Asynchronous, 4096, 4096, security);
#elif NETFRAMEWORK
NamedPipeServerStream serverPipe = new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Message, PipeOptions.Asynchronous);
#endif
await serverPipe.WaitForConnectionAsync();
var writer = new StreamWriter(serverPipe);
writer.AutoFlush = true;
var reader = new StreamReader(serverPipe);
await writer.WriteLineAsync("HELLO");
do
{
string line = await reader.ReadLineAsync();
if (line == "BYE" || line == null)
{
break;
}
await writer.WriteLineAsync(line);
} while (true);
serverPipe.Disconnect();
}
private static async Task RunClientAsync(string pipeName)
{
var clientPipe = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
await clientPipe.ConnectAsync();
var writer = new StreamWriter(clientPipe);
writer.AutoFlush = true;
var reader = new StreamReader(clientPipe);
string line = await reader.ReadLineAsync();
if (line != "HELLO")
{
throw new ApplicationException("Error");
}
await writer.WriteLineAsync("1+1=2");
line = await reader.ReadLineAsync();
if (line != "1+1=2")
{
throw new ApplicationException("Error");
}
await writer.WriteLineAsync("BYE");
clientPipe.WaitForPipeDrain();
clientPipe.Close();
}
}
@puntopaz
Copy link

On my pc, it generates a "Pipe is broken" exception on the instruction:
clientPipe.WaitForPipeDrain()
probably because the instruction:
serverPipe.Disconnect()
has already been executed.

I had to surround it with a try-except to get it working properly:

try {
  clientPipe.WaitForPipeDrain();
} catch (Exception) { }

@puntopaz
Copy link

Also, if you want a "fully-async" implementation, you could replace clientPipe.Connect() with:

var connectAction = new Action(clientPipe.Connect);
await Task.Factory.StartNew(connectAction);

or something similar

@AArnott
Copy link
Author

AArnott commented Dec 11, 2020

Nice catch on use of Connect(), @puntopaz.
Using Task.Factory.StartNew around a synchronous function doesn't actually make it purely async. It just moves the synchronous work to another thread, but still ties up a thread. Using clientPipe.ConnectAsync is preferable since it truly releases all threads while waiting for connection to proceed (or at least, that's how async I/O is expected to behave).

@mikenakis
Copy link

mikenakis commented May 14, 2022

So, I run this from a console application, and it works. Then I try running it from within a WPF application, and both functions get permanently stuck: one in await clientPipe.ConnectAsync(); the other in await serverPipe.WaitForConnectionAsync();. Does anyone have any idea as to what might be wrong? The code is exactly the same, only I had to rename Main() to Test(). I even tried replacing pipeName = @"testpipe" with pipeName = Guid.NewGuid().ToString(); no difference. It also gets stuck when I run two different instances of the WPF application, where one only runs the server and the other only runs the client.

@AArnott
Copy link
Author

AArnott commented May 14, 2022

@mikenakis: Your WPF app has a SynchronizationContext that my console app doesn't have. It's purpose is to keep async methods on the main thread that start on the main thread. But when you combine that with a Task.Wait() call (or in this case, Task.WaitAll) it'll deadlock because you're blocking the main thread that an async method needs to get back to. Make this change to fix it:

-   static void Test(string[] args)
+   static async Task Test(string[] args)
    {
        const string pipeName = @"testpipe";

        Task serverTask = RunServerAsync(pipeName);
        Task clientTask = RunClientAsync(pipeName);

-       Task.WaitAll(serverTask, clientTask);
+       await Task.WhenAll(serverTask, clientTask);
    }

@mikenakis
Copy link

I suspect it does not work under WPF because somewhere in the deep internals of the async mechanism they detect that there is a dispatcher available, and they try to make use of it, which of course miserably fails because it does not take into account the fact that we invoke Task.WaitAll(); -- Can anyone confirm my suspicion?

@mikenakis
Copy link

@AArnott thanks for the answer. I had not seen it while I was typing mine.

@mikenakis
Copy link

@AArnott yes, that was it. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment