4

I am wondering if SemaphoreSlim has anything like a priority when calling Await.

I have not been able to find anything, but maybe someone has done something like this before.

The idea is, that if I need to, an await can be called on the semaphore later on with a higher priority, and it will allow the await to return first.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
JonathanPeel
  • 743
  • 1
  • 7
  • 19
  • 1
    There is no priority. Whatever horse gets to the finishing line first wins the race. In the very unlikely case it is a tie (unlikely because you don't use WaitAll) the operating system will intentionally make the winner random, a counter-measure against lock convoys. http://joeduffyblog.com/2006/12/14/anticonvoy-locks-in-windows-server-2003-sp1-and-windows-vista/ – Hans Passant Sep 13 '16 at 16:05
  • Thank you. I thought it might be something like that. I could probably try and write something to handle what I want, but I don't think that would be a very good idea. Maybe by chance someone else will have already done something, but I am thinking of redoing some of the code. – JonathanPeel Sep 13 '16 at 16:07

2 Answers2

4

No, there are no priorities in SemaphoreSlim, whether you're using synchronous or asynchronous locking.

There is very rarely ever a need for priorities with asynchronous locks. Usually these kinds of problems have more elegant solutions if you take a step back and look at the bigger picture.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
4

Here is a class PrioritySemaphore<TPriority> that can be acquired with priority. Internally it is based on the PriorityQueue<TElement, TPriority> collection (.NET 6).

public class PrioritySemaphore<TPriority>
{
    private readonly PriorityQueue<TaskCompletionSource, (TPriority, long)> _queue;
    private readonly int _maxCount;
    private int _currentCount;
    private long _indexSeed = 0;

    public PrioritySemaphore(int initialCount, int maxCount,
        IComparer<TPriority> comparer = null)
    {
        if (initialCount < 0)
            throw new ArgumentOutOfRangeException(nameof(initialCount));
        if (maxCount <= 0)
            throw new ArgumentOutOfRangeException(nameof(maxCount));

        comparer ??= Comparer<TPriority>.Default;
        _queue = new(Comparer<(TPriority, long)>.Create((x, y) =>
        {
            int result = comparer.Compare(x.Item1, y.Item1);
            if (result == 0) result = x.Item2.CompareTo(y.Item2);
            return result;
        }));
        _currentCount = initialCount;
        _maxCount = maxCount;
    }

    public PrioritySemaphore(int initialCount, IComparer<TPriority> comparer = null)
        : this(initialCount, Int32.MaxValue, comparer) { }

    public PrioritySemaphore(IComparer<TPriority> comparer = null)
        : this(0, Int32.MaxValue, comparer) { }

    public int CurrentCount => Volatile.Read(ref _currentCount);

    public Task WaitAsync(TPriority priority)
    {
        lock (_queue)
        {
            Debug.Assert((_queue.Count == 0) || (_currentCount == 0));
            if (_currentCount > 0)
            {
                _currentCount--;
                return Task.CompletedTask;
            }
            TaskCompletionSource tcs = new(
                TaskCreationOptions.RunContinuationsAsynchronously);
            _queue.Enqueue(tcs, (priority, ++_indexSeed));
            return tcs.Task;
        }
    }

    public void Release()
    {
        TaskCompletionSource tcs;
        lock (_queue)
        {
            Debug.Assert((_queue.Count == 0) || (_currentCount == 0));
            if (_queue.Count == 0)
            {
                if (_currentCount >= _maxCount) throw new SemaphoreFullException();
                _currentCount++;
                return;
            }
            tcs = _queue.Dequeue();
        }
        tcs.TrySetResult();
    }
}

Usage example:

PrioritySemaphore<int> semaphore = new();
//...
await semaphore.WaitAsync(priority: 1);
//...
await semaphore.WaitAsync(priority: 2);
//...
semaphore.Release();

After the Release, the semaphore will be acquired by the awaiter with the highest priority. In the above example it will be the awaiter with priority 1. Smaller values denote higher priority. If there are more than one awaiters with the same highest priority, the semaphore will be acquired by the one that requested it first. Maintaining FIFO order is the reason for coupling the TPriority with a long in the above implementation.

The class PrioritySemaphore<TPriority> has only asynchronous API, and it doesn't support awaiting with cancellation or timeout. For a version that has more features and also compiles on .NET versions earlier than 6, see the 5th revision of this answer (based on the more flexible but the less efficient SortedSet).

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104