-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support async NamedPipeServerStream.WaitForConnection for synchronous pipes #63536
Comments
Tagging subscribers to this area: @dotnet/area-system-io Issue DetailsMotivating use case: My codebase is using synchronous pipe IO for IPC. The application spends a lot of CPU in the IPC mechanism, and synchronous IO uses less CPU than async IO. Thread count is not an issue, and synchronous code is simpler than async code. Sync IO is an appropriate solution. I only need async for the A synchronous
Currently, the only workaround that I know of is to create a dummy client pipe and connect to the server to break the What would it take to support this? Does the Win32 API not support overlapped IO on pipes if the pipe mode is synchronous? I could not find such a statement in the documentation. But if that is the case, An alternative solution is to make
|
Right:
Do you mean SetNamedPipeHandleState? That doesn't let you change whether the pipe was opened for overlapped I/O. |
Overlapped mode is forever because it is fundamental to a given file object. Further, the non-blocking mode (specific to named pipes) you are referring to is obsolete: (quote from Win32 ConnectNamedPipe):
Then,
While technically slightly true, I find it surprising that you would see noticeable overhead with async versus sync here? You are certainly right that sync is simpler code than async, but unless your application consists of two processes each having a single thread sending as many bytes as possible without actually processing them, I would assume the performance of that part to be negligible in the big picture outside of micro-benchmarks. Also, if it is not just 1:1 threads spamming each other, but instead, multiple such connections concurrently, I would expect the async model to behave better than the sync model at extreme scale, since the async model can result in less context switching. But even having said this, I would be surprised it would actually make any noticeable difference in a real-world application that actually does anything with the data being exchanged. All this to say, I think it would be worth biting the bullet and having it async : ). |
Thanks for the clarifications that you made. The two processes are communicating through a It seems that my suggestion of using That leaves only I was able to make cancellation work with It's unfortunate that Windows requires the IO mode to be set for the handle, and it's permanent. It would be convenient if each operation could choose what mode it wants to use. Is there a reason this design was chosen by Windows (decades ago)? |
@GSPP one more thing that you could try would be the following:
Something like this: async Task<NamedPipeServerStream> Test(CancellationToken token)
{
const uint FILE_GENERIC_READ = 0x80000000;
const uint FILE_GENERIC_WRITE = 0x40000000;
NamedPipeServerStream asyncStream = new ("test", PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
await asyncStream.WaitForConnectionAsync(token);
SafePipeHandle syncHandle = ReOpenFile(asyncStream.SafePipeHandle, FILE_GENERIC_READ | FILE_GENERIC_WRITE, (uint)FileShare.Read, (uint)FileOptions.None);
// when should the handle be disposed?!
if (syncHandle.IsInvalid)
{
int error = Marshal.GetLastPInvokeError();
throw new Exception($"ReOpenFile failed with {error}");
}
return new NamedPipeServerStream(PipeDirection.InOut, isAsync: false, isConnected: true, syncHandle);
}
[DllImport("kernel32.dll", SetLastError = true)]
public static extern SafePipeHandle ReOpenFile(SafePipeHandle hOriginalFile, uint dwAccess, uint dwShareMode, uint dwFlags); Usually mixing sync and async IO is a bad idea which often leads to deadlocks and that is why I don't think that we should allow for switching from sync<=>async even if it's possible using some hacks.
I am sorry but I don't know. I can imagine that it has simplified the design a LOT. |
@adamsitnik Thank you very much! I hadn't considered this approach before, and I didn't find this idea during my research. The internet is silent on the idea that you can reopen a named pipe. But it's only logical since named pipes seem to be implemented ontop of normal file objects. (Thanks for even including source code!) My first attempt was to reopen the file async just for the
I have tried studying the ReactOS code with respect to sync IO and overlapped IO processing. So far I was unable to penetrate it enough to find the core processing functionality of that. I kind of want to see the data structures used to get a clue for why this was done. The original Windows kernel developers are known to be very high-caliber people, so I'm sure they had reasons in mind. For interested future visitors, I'm linking to the ReactOS (open source Windows clone) source code for the named pipe file system driver: https://doxygen.reactos.org/d2/d9d/drivers_2filesystems_2npfs_2main_8c.html The tree on the left shows all files for that driver. I tried studying that to find a way to break the wait, without success. The fact that I also wonder why exiting the process cleans up the pipe but closing the handle does not. At least, that's the way it's supposed to happen. Or maybe, there's a kernel level memory leak or hang of some kind in this case? My understanding is that process termination is implemented by simply closing all handles. A process will remain alive in a zombie state as long as some IO is left. Somehow, Windows seems to handle this. Here is the code for the internal static void WaitForConnectionCancellable4(NamedPipeServerStream pipeStream, CancellationToken cancellationToken)
{
if (pipeStream.IsAsync)
throw new ArgumentException("PipeStream must be synchronous.", nameof(pipeStream));
//Synchronize so that CancelSynchronousIo is not called after the WaitForConnection operation has completed.
var threadID = NativeMethods.GetCurrentThreadId();
//The thread must be opened at a point where it is guaranteed to exist. Can't open it in Cancel.
using var threadHandle = NativeMethods.OpenThread(NativeMethods.ThreadAccess.TERMINATE, false, threadID);
if (threadHandle.IsInvalid)
throw new Win32Exception();
bool isCompleted = false;
object lockObject = new();
void Cancel()
{
lock (lockObject)
{
if (isCompleted)
return;
//There is a potential race condition in that cancellation might happen before WaitForConnection has started.
//Closing the handle denies further calls to WaitForConnection.
pipeStream.Dispose();
NativeMethods.CancelSynchronousIo(threadHandle.DangerousGetHandle()); //Nothing can be done about errors.
}
}
using var cancellationTokenRegistration = cancellationToken.Register(Cancel);
try
{
pipeStream.WaitForConnection();
}
finally
{
lock (lockObject)
{
isCompleted = true;
}
}
}
[TestMethod]
public void PipeHelper_WaitForConnectionCancellable4()
{
foreach (var cancel in new[] { false, true })
{
var serverPipeName = "Test-" + Guid.NewGuid().ToString("N");
using var serverPipe = new NamedPipeServerStream(serverPipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.None);
var cts = new CancellationTokenSource();
var waitTask = Task.Run(() => PipeHelper.WaitForConnectionCancellable4(serverPipe, cts.Token));
if (cancel)
{
Thread.Sleep(100); //Give the task time to start
cts.Cancel();
waitTask.WhenCompleted().Wait();
Assert.IsTrue(waitTask.Exception.InnerException is OperationCanceledException);
}
else
{
using var clientPipe = new NamedPipeClientStream(".", serverPipeName, PipeDirection.InOut, PipeOptions.None);
clientPipe.Connect(timeout: 0); //This validates that the WaitForConnection call had time to start.
Assert.IsTrue(clientPipe.IsConnected);
waitTask.Wait();
Assert.IsTrue(serverPipe.IsConnected);
}
}
} |
Motivating use case: My codebase is using synchronous pipe IO for IPC. The application spends a lot of CPU in the IPC mechanism, and synchronous IO uses less CPU than async IO. Thread count is not an issue, and synchronous code is simpler than async code. Sync IO is an appropriate solution.
I only need async for the
WaitForConnection
call because the child process might die and never connect. A timeout is not possible. Instead, I'm listening for process exit to terminate the pipe.A synchronous
WaitForConnection
call is really hard to break. For example, closing the pipe handle (Dispose
) does not unblock that call... Probably,CancelSynchronousIo
could do it, but I read in another issue that this is hard to implement.WaitForConnectionAsync
works on a synchronous pipe, but it simply delegates the sync call to the thread-pool with no ability to cancel. That call will hang forever. I wonder if this implicit async-over-sync design might be a foot gun for people.Currently, the only workaround that I know of is to create a dummy client pipe and connect to the server to break the
WaitForConnection
call. This scheme is not the nicest solution in the world, and I wonder if there are circumstances where it would break (security?).What would it take to support this? Does the Win32 API not support overlapped IO on pipes if the pipe mode is synchronous? I could not find such a statement in the documentation. But if that is the case,
SetNamedPipeHandleMode
can temporarily switch the mode to async. Is this feasible to support? This behavior would then be transparent to callers ofWaitForConnectionAsync
.An alternative solution is to make
IsAsync
settable.ReadMode
already is settable, so the infrastructure and precedence for callingSetNamedPipeHandleMode
are there.The text was updated successfully, but these errors were encountered: