Search code examples
c#.net-5cancellationtokensource

How to cancel SemaphoreSlim WaitAsync method


Consider the following code, executed with .NET 5:

using System;
using System.Threading;

public class Program
{
    public static void Main(string[] args)
    {
        var semaphore = new SemaphoreSlim(0);

        var cts = new CancellationTokenSource();

        var entrance = semaphore.WaitAsync(cts.Token);

        cts.Cancel();
        cts.Dispose();

        semaphore.Release();

        Console.WriteLine("Entrance status: " + entrance.Status);
        Console.WriteLine("Current count: " + semaphore.CurrentCount);
    }
}

When I run this code, the application complete successfully and I got the following result:

Entrance status: WaitingForActivation

Current count: 0

But since I cancel the WaitAsync operation before releasing the semaphore, I was expecting the semaphore CurrentCount to be 1 and the Task to be in the Canceled status.

Before posting the question, I ran the code within https://dotnetfiddle.net and surprisingly, it ran as I expected with the .NET Framework 4.7.2.

Do I find a bug in .NET 5 SemaphoreSlim?

Is there a way to get the former behavior in .NET 5?

PS: I find a way to get the former behavior by setting a timeout to the WaitAsync operation, but that's not an acceptable answer to my opinion.

EDIT according to Guru Stron comments

Awaiting entrance before the Release statement produces an OperationCancelledException as I would expect.

But awaiting after the statement does not throw any exception and the semaphore is "consumed".

Where both cases produce an error in former .NET Framework.


Solution

  • CancellationTokenSource.Cancel is a request to cancel. The cancellation tokens become canceled immediately, but Cancel does not guarantee to wait, blocking the current thread, until all operations listening to that cancellation token have been canceled (and their parent operations, etc).

    In other words, your code has an inherent race condition: whether the Release or the CancellationToken's cancellation will reach the WaitAsync operation first. If the Release reaches it first, then the semaphore will be acquired. If the cancellation reaches it first, then the wait will be canceled.

    awaiting the WaitAsync operation's task before calling Release resolves this race condition by (a)waiting the operation, forcing it to see the cancellation before the Release is sent.

    Do I find a bug in .NET 5 SemaphoreSlim?

    No. The code was depending on a race condition, both on .NET Framework and on .NET 5. The result of the race condition is not guaranteed to have any specific result on either platform.

    Is there a way to get the former behavior in .NET 5?

    No. I recommend reworking the code so that it does not depend on a race condition. Then it will work correctly for both platforms.

    It's possible that the code is using SemaphoreSlim for something it wasn't designed for. E.g., if you need a queue of asynchronous work, then build a queue of asynchronous work instead of trying to use a SemaphoreSlim as one.