Open h3xds1nz opened 1 month ago
Tagging subscribers to this area: @mangod9 See info in area-owners.md if you want to be subscribed.
Taking latest language features into account the proposal could be
namespace System.Threading;
public partial class SynchronizationContext
{
+ public virtual int Wait(params ReadOnlySpan<IntPtr> waitHandles, bool waitAll, int millisecondsTimeout);
+ protected static int WaitHelper(params ReadOnlySpan<IntPtr> waitHandles, bool waitAll, int millisecondsTimeout);
}
ReadOnlySpan
as the waitHandles aren't updatedparams ReadOnlySpan
so that 1 or more waithandles can be given more easilyBut as given above this wouldn't compile, as the params
parameter must be last, so actually it could be
namespace System.Threading;
public partial class SynchronizationContext
{
+ public virtual int Wait(bool waitAll, int millisecondsTimeout, params ReadOnlySpan<IntPtr> waitHandles);
+ protected static int WaitHelper(bool waitAll, int millisecondsTimeout, params ReadOnlySpan<IntPtr> waitHandles);
}
Can you elaborate on why this particular allocation is impactful to you? What SC are you using where you've asked for wait notification and what's the calling pattern where you're frequently doing blocking waits on threads with this context current?
@gfoidl The second proposal could do, this would also aid to the fact with Span<T>
overloads being preferred over T[]
and since those methods are virtual which I didn't consider before.
However, since the underlying WaitHandle
methods use Span<IntPtr>
, you would be unable to call those when we use ReadOnlySpan<IntPtr>
unless the chain is adjusted, unsure whether it is modified anywhere. That is why I didn't initially go with ROS.
@stephentoub Thank you for your interest on this. I originally came onto this alloc when looking at total GC time.
The most usual chain here for me were Dispatcher
Invokes in WPF and the underlying operations, queue posts from long-running background threads to update UI, which uses ManualResetEvent
for DispatcherOperationEvent
.
However, since the underlying
WaitHandle
methods useSpan<IntPtr>
, you would be unable to call those when we useReadOnlySpan<IntPtr>
unless the chain is adjusted, unsure whether it is modified anywhere.
This gets addressed in https://github.com/dotnet/runtime/pull/104864.
This gets addressed in #104864.
Amazing, thanks for that, I have updated the proposal based on your suggestion.
Can you elaborate on why this particular allocation is impactful to you? What SC are you using where you've asked for wait notification and what's the calling pattern where you're frequently doing blocking waits on threads with this context current?
Here's an example of how I'm using this (not publicly accessible link) : https://github.com/dotnet/paint.net/blob/bc539c00f25e9f5bcafb4146cfc449532cfb29b9/Windows.Framework/Threading/TimerThread.cs#L250
The use here is clumsy, but not performance impacting. I'd love to see this become less clumsy, but it's not a high priority for my uses.
Because there's no span-based versions of these APIs, I have to use new WaitHandle[]
. I cannot use ArrayPool<WaitHandle>.Shared.Rent()
and then slice the span to the appropriate length. In my case I only need to allocate once at app startup, so it's no big deal, but any kind of high-traffic situation becomes more clumsy as you need to implement your own local array pooling.
The code is essentially this. So I can't use the array pool, and it's necessary to allocate two arrays for the two waits that use 3 and 2 handles respectively.
This is for WaitHandle.Wait[Any|All]()
but it's the same idea as SynchronizationContext.Wait()
.
private void TimerThreadProc()
{
WaitHandle[] waitHandles3 = new WaitHandle[3];
waitHandles3[0] = this.isCancelledEvent;
waitHandles3[1] = this.isEnabledEvent;
waitHandles3[2] = this.pulseEvent;
WaitHandle[] waitHandles2 = new WaitHandle[2];
waitHandles2[0] = this.isCancelledEvent;
waitHandles2[2] = this.waitableTimer; // Win32 waitable timer
while (true)
{
// wait on the 3 handles array
int waitIndex = WaitHandle.WaitAny(waitHandles3);
...
// wait on the 2 handles array
waitIndex = WaitHandle.WaitAny(waitHandles2);
...
}
}
Background and motivation
Currently,
SynchronizationContext
has apublic virtual int Wait(...)
method that takes array an ofIntPtr
and a protectedWaitHelper
method which also takes an array ofIntPtr
, which is by default called by the public member.One of the frequent call chains originate from
WaitOne
->WaitOneNoCheck
fromWaitHandle
class and its derivations, which is currently always forced to allocate a heap-based array of a singleIntPtr
, causing unnecessary GC overhead.Since the protected
WaitHelper
helper fromSynchronizationContext
calls intoWaitHandle.WaitMultipleIgnoringSyncContext(...)
which already takes aReadOnlySpan<IntPtr>
, by adding publicReadOnlySpan<IntPtr>
overloads ontoSynchronizationContext
we could get rid of this unnecessary allocation when awaiting a single handle in the runtime code, and also in user-code.API Proposal
API Usage