Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.

Use RunContinuationsAsynchronously in SemaphoreSlim.WaitAsync #22686

Merged
merged 1 commit into from
Feb 19, 2019

Conversation

stephentoub
Copy link
Member

SemaphoreSlim.Release shouldn't invoke arbitrary continuations as part of its invocation, so when it dequeues a task waiter and goes to complete it, rather than just calling TrySetResult, it queues the task to the thread pool to have TrySetResult invoked there. Now that we have RunContinuationsAsynchronously, though, we can just use that functionality instead. This has two main benefits:

  1. We avoid queueing a work item entirely if there are no continuations from the task. This might happen, for example, if the semaphore is released so quickly after waiting on it that the WaitAsync caller hasn't yet hooked up a continuation, in which case the await on the WaitAsync task will just see IsCompleted as true and continue running.
  2. We avoid queueing a work item when the task represents a synchronous Wait, which happens if there's already a pending WaitAsync when Wait goes to block. The main benefit here is avoiding potential thread pool starvation, if threads in the pool are blocked in such Waits, and previously another thread pool thread would have been needed to run the queued work item to complete the synchronous Wait.

Fixes https://github.com/dotnet/corefx/issues/35393
cc: @kouvel, @mgravell, @benaadams

SemaphoreSlim.Release shouldn't invoke arbitrary continuations as part of its invocation, so when it dequeues a task waiter and goes to complete it, rather than just calling TrySetResult, it queues the task to the thread pool to have TrySetResult invoked there.  Now that we have RunContinuationsAsynchronously, though, we can just use that functionality instead.  This has two main benefits:
1. We avoid queueing a work item entirely if there are no continuations from the task.  This might happen, for example, if the semaphore is released so quickly after waiting on it that the WaitAsync caller hasn't yet hooked up a continuation, in which case the await on the WaitAsync task will just see IsCompleted as true and continue running.
2. We avoid queueing a work item when the task represents a synchronous Wait, which happens if there's already a pending WaitAsync when Wait goes to block.  The main benefit here is avoiding potential thread pool starvation, if threads in the pool are blocked in such Waits, and previously another thread pool thread would have been needed to run the queued work item to complete the synchronous Wait.
@benaadams
Copy link
Member

benaadams commented Feb 19, 2019

Nice and it will just pick up on ITaskCompletionAction.InvokeMayRunArbitraryCode automatically?

@stephentoub
Copy link
Member Author

it will just pick up on ITaskCompletionAction.InvokeMayRunArbitraryCode automatically?

Exactly.

@mgravell
Copy link
Member

looks great to me - simple and elegant, thanks

@stephentoub stephentoub merged commit e2081d0 into dotnet:master Feb 19, 2019
@stephentoub stephentoub deleted the semwait branch February 19, 2019 22:35
@pianoman4873
Copy link

Is this fix also implemented in the .NET Framework ?

@stephentoub
Copy link
Member Author

This was primarily a perf improvement and it was not backported.

@pianoman4873
Copy link

pianoman4873 commented Aug 28, 2020

@stephentoub Thanks. It caused the same issues reported by @mgravell at a 3rd party software the company I work in uses , and they've identified it with in their tests.
Any suggestions if we still want to use the SemaphoreSlim in high load in the .NET framework ?
Also, I wouldn't say that a fix to the "spiral of death" as Mark put it is a mere perf improvement ... the application grinds to a halt due to this unfortunately.

@mgravell
Copy link
Member

mgravell commented Aug 28, 2020

@pianoman4873 there's a whole bunch of fixes on a wide range of topics: freely and widely available. It is called ".NET Core" (or soon: .NET 5).

If you're in .NET Framework, don't expect changes. What works, works. What doesn't: doesn't.

@pianoman4873
Copy link

pianoman4873 commented Aug 29, 2020

Thanks @mgravell - thanks, but the least I would expect is that after finding such critical bugs and not fixing them is to document them. Without this - the reliability of .NET framework and Microsoft as a company totally diminishes.

picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
…/coreclr#22686)

SemaphoreSlim.Release shouldn't invoke arbitrary continuations as part of its invocation, so when it dequeues a task waiter and goes to complete it, rather than just calling TrySetResult, it queues the task to the thread pool to have TrySetResult invoked there.  Now that we have RunContinuationsAsynchronously, though, we can just use that functionality instead.  This has two main benefits:
1. We avoid queueing a work item entirely if there are no continuations from the task.  This might happen, for example, if the semaphore is released so quickly after waiting on it that the WaitAsync caller hasn't yet hooked up a continuation, in which case the await on the WaitAsync task will just see IsCompleted as true and continue running.
2. We avoid queueing a work item when the task represents a synchronous Wait, which happens if there's already a pending WaitAsync when Wait goes to block.  The main benefit here is avoiding potential thread pool starvation, if threads in the pool are blocked in such Waits, and previously another thread pool thread would have been needed to run the queued work item to complete the synchronous Wait.

Commit migrated from dotnet/coreclr@e2081d0
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants