-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Bad performance: launching processes in parallel on Win32 / Windows #31556
Comments
I do not know why that lock is static. It is so in the internal code back to at least 2010 after which I do not currently know how to find history. It's only used for Windows. @stephentoub do you know? |
any news? |
I know there was a reason for it, as this question has come up before and there was discussion about it, but I just spent a while searching and can't find remnants of those discussions. |
I can't see any reason, too. I have checked the final assembly code (including all other parts). The variable s_createProcessLock is only used in StartWithCreateProcess. But there seems to be no static data to protect. Via blame I can see it was moved 5 years ago to winodows only in a very huge commit, which refactored the Process library to have a PAL. Before this it was in the "initial commit" - so in this repository there is no further history... For my project I could access win api directly via PInvoke, but that is something I would really like to avoid because it breaks portability... |
@jkotas do you recall any context? |
This lock is trying fix the problem with the inheritable stdin/stdout/stderr handles. We need to make sure that only one process inherits them. If the redirected handle gets inherited by multiple processes starting in parallel by accident, bad things happen. For the record, here is the .NET Framework change that introduced the lock:
|
The fix to remove this lock is to use UpdateProcThreadAttribute and |
While you are on it, you can also replace runtime/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs Line 862 in 4f9ae42
with just |
Won't that exclude from being inherited other handles in the process that should have been, e.g. any created by AnonymousPipeServerStream? |
Ok, I have not realized that other components may be depending on the |
In my case I have explicitly redirected all handles via RedirectStandardOutput / RedirectStandardError / RedirectStandardInput. So the API should create some Pipes. Is it still required to do some locking? |
You don't need to do any, but the implementation still does, yes. Let's say you launch two processes concurrently. If one of them redirects standard input/output/error, that means it's opening new handles to be inherited into the child process to use for communication between the parent and child. But those handles could also end up getting inherited by the other concurrently started child process. |
Up to now I dont got the point. => The lock should be aquired only if not all standard inputs / outputs are redirected. If it is not possible, the locked region should be smaller (e.g. getting the working directory ...). |
When it goes to start a child process, it creates an anonymous pipe with an inheritable handle for the child: the child will inherit that handle, which is how it communicates with the parent. If any other process is started at the same time, it would also inherit that same handle that wasn't intended for it, such that both child processes inherited the same handle. That's the problem the lock is avoiding. Jan's suggestion was to explicitly tell the child process which handle to inherit (i.e. "inherit these and only these"), which would address this, because then an unrelated child wouldn't inherit the other's handle, but that's problematic for other reasons, because other parts of the system were designed with the expectation that their handles would be inherited by children.
It's not about the working directly. It's about making the whole region around handle creation / process starting / handle destruction atomic. |
Thanks for your fast response. Thats what I don't understand:
As far as I can see these handles are submitted explicitly in the startup info:
The pipes (and their handles) are created during that call (just true if redirected). So there should be no sharing in this case. |
They are; that's how the child process knows which handle to use for what. But in order to actually use the handles, it needs access to them, so the handles are created as inheritable. If the handles weren't inheritable, the handle values passed to the child would be meaningless. It'd be like me saying to you "please read from the email I just emailed you" but then not actually sending you an email. Look in the code just below the three lines you copied: runtime/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs Lines 519 to 523 in 4d3ec15
then see the docs for that STARTF_USESTDHANDLES option:https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa "The hStdInput, hStdOutput, and hStdError members contain additional information. If this flag is specified when calling one of the process creation functions, the handles must be inheritable and the function's bInheritHandles parameter must be set to TRUE. For more information, see Handle Inheritance." |
That was fast. So we could use for example file handles in the child application which could be submitted via Window Message. But if Then there is no conflict with other starting processes. |
There is. If you could guarantee that every child process the parent creates has all of its RedirectXx properties set to false, then the lock wouldn't be necessary. But the moment it's possible that even just one child process gets created with one of its RedirectXx properties set to true, the lock is needed. In that case, the parent process will be creating a handle to pass to the child. It makes that handle inheritable so that the child will have access to it, and it stores the handle value into the hStdXx field so that the child will know which handle it should use for that purpose. But because that handle was made inheritable, it's inheritable not just to that child, but to any other child created while the handle is alive. As such, Process needs to make sure that no other child process is created while that handle is alive... hence the lock. |
I would say just the opposite. The child gets handles to the pipes and not to the normal standard XXX if RedirectXXX is true. A second call to process start would create three new pipes which have no relationship to the other pipes.
|
It's true that these (private) handles are inherited as well. But open file handles, sockets..., too. |
The problem is that these other child processes can end up keeping the handle alive, which can then cause the parent and actual child that's using the handle to misbehave, e.g. the actual child can close the handle to signal completion to the parent, but the parent won't see it closed until all children that inherited the handle close it as well, which they won't do (since as you say they don't know the handle value) until they exit.
Most handles are created as non-inheritable by default. In general you need to opt-in to them being inheritable. For example, sockets are created with WSA_FLAG_NO_HANDLE_INHERIT: runtime/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Windows.cs Line 44 in 4d3ec15
and FileStreams are created with a FileShare, which needs to explicitly specify Inheritable for the handle to be created as such: https://github.com/dotnet/runtime/blob/master/src/libraries/System.Private.CoreLib/src/System/IO/FileShare.cs |
But you can't know what the child is doing. Of course I don't know how these handles are manged internally. But I can Imagine that a call to DuplicateHandle just increases a reference count to support multiple Close calls before freeing the underling data structures. In addition just the childXXXPipeHandle is disposed in the lock. This means that that the parent can't write/read somthing from/in the childXXX Pipe. This is Okay because the parent just want's to get the results / outputs. It never writes into the output stream or reads from the input stream! The "real" handles remain open and are stored in the FileStreams / StreamReaders (outside the lock!). You can see a working example (without locks) here: |
I feel like we're either talking past each other or I'm not explaining things well. I will let someone else chime in. Regardless, the lock can't be removed. |
I think this issue can be closed in favor of #13943 -- it seems the way forward is a new API. |
similar on Linux https://github.com/dotnet/corefx/issues/33069 |
Would it make sense to use a readers-writer lock instead of a mutex here? That way, multiple processes that don't need the handle can be started in parallel, while if a process does need the handle, it's guaranteed no other process is starting in the meantime. |
It's an interesting idea that could be experimented with, but I'm a little skeptical it'll be worthwhile. Process.Start calls where no redirection is requested would take the reader lock. If any of the RedirectXx properties was true, it would take the writer lock. And I suspect that a typical app launching processes at enough scale for this to matter is going to be redirecting, which means they'd be serialized anyway. If switching to a reader/writer lock were free, then we might as well, but it's not, in particular in that such locks have measurably higher overhead than normal locks. But, if someone wants to experiment with it, it'd be interesting to see the numbers. |
I would really like to launch multiple worker-processes in parallel with minimal blocking.
My code is heavily using async / await. Unfortunately the overall perfomance is not as good as I would expect.
For me the perfomance killer seems to be line 479
lock (s_createProcessLock)
which disallows effectively parallel process launching.Why is the lock static?
The text was updated successfully, but these errors were encountered: