Skip to content
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

Reduce cost of async waiting on SemaphoreSlim with cancellation/timeout #83294

Merged
merged 1 commit into from Mar 11, 2023

Conversation

stephentoub
Copy link
Member

If a WaitAsync on a SemaphoreSlim is unable to synchronously acquire the semaphore, it calls an async method that handles the waiting. That method awaits with a custom awaiter that avoids throwing in the case of cancellation or timeout. However, in using a custom awaiter, we get knocked off the highest-perf path that can avoid allocating an Action. This change puts it back onto the golden path by having the awaiter implement our internal interface that lets it backchannel with the async method builders. Eventually we want this awaiter to be available publicly as well, and it'll end up benefiting more than just semaphores.

Method Toolchain Mean Ratio Allocated Alloc Ratio
WaitAsync \main\corerun.exe 57.39 us 1.00 43.84 KB 1.00
WaitAsync \pr\corerun.exe 52.43 us 0.91 37.58 KB 0.86
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Threading;
using System.Threading.Tasks;

[MemoryDiagnoser]
public partial class Program
{
    static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);

    private CancellationToken _token = new CancellationTokenSource().Token;
    private SemaphoreSlim _sem = new SemaphoreSlim(0);
    private Task[] _tasks = new Task[100];

    [Benchmark]
    public Task WaitAsync()
    {
        for (int i = 0; i < _tasks.Length; i++)
        {
            _tasks[i] = _sem.WaitAsync(_token);
        }
        _sem.Release(_tasks.Length);
        return Task.WhenAll(_tasks);
    }
}

If a WaitAsync on a SemaphoreSlim is unable to synchronously acquire the semaphore, it calls an async method that handles the waiting.  That method awaits with a custom awaiter that avoids throwing in the case of cancellation or timeout.  However, in using a custom awaiter, we get knocked off the highest-perf path that can avoid allocating an Action.  This change puts it back onto the golden path by having the awaiter implement our internal interface that lets it backchannel with the async method builders. Eventually we want this awaiter to be available publicly as well, and it'll end up benefiting more than just semaphores.
@stephentoub stephentoub added this to the 8.0.0 milestone Mar 11, 2023
@ghost ghost assigned stephentoub Mar 11, 2023
@ghost
Copy link

ghost commented Mar 11, 2023

Tagging subscribers to this area: @mangod9
See info in area-owners.md if you want to be subscribed.

Issue Details

If a WaitAsync on a SemaphoreSlim is unable to synchronously acquire the semaphore, it calls an async method that handles the waiting. That method awaits with a custom awaiter that avoids throwing in the case of cancellation or timeout. However, in using a custom awaiter, we get knocked off the highest-perf path that can avoid allocating an Action. This change puts it back onto the golden path by having the awaiter implement our internal interface that lets it backchannel with the async method builders. Eventually we want this awaiter to be available publicly as well, and it'll end up benefiting more than just semaphores.

Method Toolchain Mean Ratio Allocated Alloc Ratio
WaitAsync \main\corerun.exe 57.39 us 1.00 43.84 KB 1.00
WaitAsync \pr\corerun.exe 52.43 us 0.91 37.58 KB 0.86
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Threading;
using System.Threading.Tasks;

[MemoryDiagnoser]
public partial class Program
{
    static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);

    private CancellationToken _token = new CancellationTokenSource().Token;
    private SemaphoreSlim _sem = new SemaphoreSlim(0);
    private Task[] _tasks = new Task[100];

    [Benchmark]
    public Task WaitAsync()
    {
        for (int i = 0; i < _tasks.Length; i++)
        {
            _tasks[i] = _sem.WaitAsync(_token);
        }
        _sem.Release(_tasks.Length);
        return Task.WhenAll(_tasks);
    }
}
Author: stephentoub
Assignees: -
Labels:

area-System.Threading

Milestone: 8.0.0

Copy link
Member

@davidfowl davidfowl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is cheating 😃

@stephentoub stephentoub merged commit a524575 into dotnet:main Mar 11, 2023
@stephentoub stephentoub deleted the semslimwait branch March 11, 2023 16:36
@dotnet dotnet locked as resolved and limited conversation to collaborators Apr 10, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants