4

When using WaitHandle.WaitAny and Semaphore class like the following:

var s1 = new Semaphore(1, 1);
var s2 = new Semaphore(1, 1);

var handles = new [] { s1, s2 };

var index = WaitHandle.WaitAny(handles);

handles[index].Release();

It seems guaranteed that only one semaphore is acquired by WaitHandle.WaitAny.

Is it possible to obtain similar behavior for asynchronous (async/await) code?

Community
  • 1
  • 1
drowa
  • 682
  • 5
  • 13
  • You mean [Task.WaitAny()](https://msdn.microsoft.com/en-us/library/Dd270672(v=VS.110).aspx) ? – Erik Philips Jul 25 '15 at 00:34
  • I think it doesn't matter because it seems `Task.WaitAny` is just the synchronous version of `Task.WhenAny`. – drowa Jul 25 '15 at 00:36
  • I was going to post [Task.WhenAny()](https://msdn.microsoft.com/en-us/library/system.threading.tasks.task.whenany(v=vs.110).aspx) as well. – Erik Philips Jul 25 '15 at 00:37
  • You should up vote the question then. – drowa Jul 25 '15 at 00:48
  • 1
    @drowa: This is almost certainly the wrong solution to whatever problem you have. What is the actual problem you're trying to solve? – Stephen Cleary Jul 25 '15 at 17:03
  • @StephenCleary: I want to share a pool of resources. – drowa Jul 25 '15 at 17:05
  • @drowa: And why do you need two semaphores? – Stephen Cleary Jul 25 '15 at 17:06
  • @StephenCleary: This is just an example. In the real case we would have n semaphores, one for each resource in the pool. – drowa Jul 25 '15 at 17:10
  • @drowa: You're using one semaphore per resource to determine whether a resource is in use or not? There are better approaches. – Stephen Cleary Jul 25 '15 at 17:23
  • @StephenCleary: I'm using a semaphore to coordinate access to a shared resource; it's the canonical use of semaphores, by the way. What better approach could I use? – drowa Jul 25 '15 at 17:30
  • @StephenCleary: Note in the synchronous example I gave, `index` is fundamental. It indicates, to the current thread, what resource from the pool has been assigned for use; you can imagine the pool as an array containing two resources. – drowa Jul 25 '15 at 18:02
  • @drowa: You can use a standard collection protected by a single lock. – Stephen Cleary Jul 25 '15 at 23:25
  • @StephenCleary: I don't see that single lock approach working out well. I created a question specifically for the situation we are discussing here: http://stackoverflow.com/questions/31640158. It would be great if you can explain your approach as an answer there. – drowa Jul 26 '15 at 19:05

3 Answers3

1

I cannot think of a built-in solution. I'd do it like this:

var s1 = new SemaphoreSlim(1, 1);
var s2 = new SemaphoreSlim(1, 1);

var waits = new [] { s1.WaitAsync(), s2.WaitAsync() };

var firstWait = await Task.WhenAny(waits);

//The wait is still running - perform compensation.
if (firstWait == waits[0])
 waits[1].ContinueWith(_ => s2.Release());
if (firstWait == waits[1])
 waits[0].ContinueWith(_ => s1.Release());

This acquires both semaphores but it immediately releases the one that came second. This should be equivalent. I cannot think of a negative consequence of acquiring a semaphore needlessly (except performance of course).

usr
  • 168,620
  • 35
  • 240
  • 369
  • 1
    It is a good start. The "leakage" of the task returned by `ContinueWith` doesn't smell good though. – drowa Jul 25 '15 at 20:17
  • I like to track the tasks I create. For example, making sure they are all completed before the termination of the application starts. – drowa Jul 25 '15 at 20:29
  • 1
    I see, I like to do that as well. Especially useful for error tracking. Solution: Make the WaitAsync's take a CancellationToken. Signal the token of the wait to abort. Then, await the continuation task as well as the wait task. The wait should complete pretty much immediately and all tasks are properly shut down. (I will not add that code. Too much.) – usr Jul 25 '15 at 20:34
  • I was thinking about that solution as well. – drowa Jul 25 '15 at 20:36
  • I'm wondering what would be the synchronous version of this (i.e. still using `SemaphoreSlim`). We would probably need to use `SemaphoreSlim.AvailableWaitHandle`. – drowa Aug 13 '15 at 20:05
  • @drowa a synchronous version would by calling Wait on the resulting task. All internal stuff happens asynchronously. The wait is then bolted on. Never use wait handles if you can help it. They are clumsy and must be disposed. Tasks are slick and must not be disposed, not even when waiting (in recent .NET versions). – usr Aug 13 '15 at 20:41
1

Here is a generalized implementation of a WaitAnyAsync method, that acquires asynchronously any of the supplied semaphores:

/// <summary>
/// Asynchronously waits to enter any of the semaphores in the specified array.
/// </summary>
public static async Task<SemaphoreSlim> WaitAnyAsync(SemaphoreSlim[] semaphores,
    CancellationToken cancellationToken = default)
{
    // Fast path
    cancellationToken.ThrowIfCancellationRequested();
    var acquired = semaphores.FirstOrDefault(x => x.Wait(0));
    if (acquired != null) return acquired;

    // Slow path
    using var cts = CancellationTokenSource.CreateLinkedTokenSource(
        cancellationToken);
    Task<SemaphoreSlim>[] acquireTasks = semaphores
        .Select(async s => { await s.WaitAsync(cts.Token); return s; })
        .ToArray();

    Task<SemaphoreSlim> acquiredTask = await Task.WhenAny(acquireTasks);

    cts.Cancel(); // Cancel all other tasks

    var releaseOtherTasks = acquireTasks
        .Where(task => task != acquiredTask)
        .Select(async task => (await task).Release());

    try { await Task.WhenAll(releaseOtherTasks); }
    catch (OperationCanceledException) { } // Ignore
    catch
    {
        // Consider any other error (possibly SemaphoreFullException or
        // ObjectDisposedException) as a failure, and propagate the exception.
        try { (await acquiredTask).Release(); } catch { }
        throw;
    }

    try { return await acquiredTask; }
    catch (OperationCanceledException)
    {
        // Propagate an exception holding the correct CancellationToken
        cancellationToken.ThrowIfCancellationRequested();
        throw; // Should never happen
    }
}

This method becomes increasingly inefficient as the contention gets higher and higher, so I wouldn't recommend using it in hot paths.

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

Variation of @usr's answer that solved my slightly more general problem (after quite some time going down the rathole of trying to marry AvailableWaitHandle with Task...)

    class SemaphoreSlimExtensions
    
        public static Task AwaitButReleaseAsync(this SemaphoreSlim s) => 
            s.WaitAsync().ContinueWith(_t -> s.Release(), ct,
                TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnRanToCompletion,
                TaskScheduler.Default);
        public static bool TryTake(this SemaphoreSlim s) => 
            s.Wait(0);

In my use case, the await is just a trigger for synchronous logic that then walks the full set - the TryTake helper is in my case a natural way to handle the conditional acquisition of the semaphore and the processing that's contingent on that.

var sems = new[] { new SemaphoreSlim(1, 1), new SemaphoreSlim(1, 1) };

await Task.WhenAny(from s in sems select s.AwaitButReleaseAsync());

Putting it here as I believe it to be clean, clear and relatively efficient but would be happy to see improvements on it

Ruben Bartelink
  • 59,778
  • 26
  • 187
  • 249
  • This use of `ContinueWith` violates the guideline [CA2008: Do not create tasks without passing a TaskScheduler](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2008). Also I don't understand what the `AwaitButReleaseAsync` accomplishes in your example. What are you supposed to do after awaiting the `Task.WhenAny(from s in sems select s.AwaitButReleaseAsync())`? – Theodor Zoulias Aug 04 '22 at 02:49
  • Thanks for the feedback, it's used in https://github.com/jet/propulsion/search?q=AwaitButRelease - edited in the `TaskContinuationOptions.ExecuteSynchronously` (would you agree that obviates the `CA2008` point?). – Ruben Bartelink Aug 04 '22 at 06:55
  • As for the use case, this allows me to separate 2/3 triggers from the associated actions. I could be waiting for a) any of 3 of 5 output limiters to drop b) a new set of inputs to arrive. Being able to wait on the 3 output limits without being handed 0-3 semaphores that I've just 'gained' means I can instead go do that without having to figure out and/or pass the context of which ones got locked. (The other complicating factor is that despite waiting for items 2, 3 and 5, my logic needs to visit each one in order - even if I got triggered on 3, I want to submit 1 if available, then 2 etc...) – Ruben Bartelink Aug 04 '22 at 07:06
  • wait: https://github.com/jet/propulsion/blob/c1c67e56bb329061f687e7b0282d8ec215fd9539/src/Propulsion/Submission.fs#L120-L125 process: https://github.com/jet/propulsion/blob/c1c67e56bb329061f687e7b0282d8ec215fd9539/src/Propulsion/Submission.fs#L81-L88 – Ruben Bartelink Aug 04 '22 at 07:08
  • 1
    No. The `TaskContinuationOptions.ExecuteSynchronously` is just a hint, that the `TaskScheduler.Current` is not obliged to respect. In order to conform with the guideline you must specify explicitly the `scheduler`, most likely the `TaskScheduler.Default`. – Theodor Zoulias Aug 04 '22 at 07:09
  • As for the purpose that the `AwaitButReleaseAsync` exists, it's still obscure to me. Are you sure that your answer is relevant to the question asked? The OP has asked for an asynchronous version of `WaitHandle.WaitAny`. Does the `AwaitButReleaseAsync` help at solving this problem? And if it does, how? – Theodor Zoulias Aug 04 '22 at 07:15
  • 1
    Thanks for the scheduler point, will fix. You have a point in saying that I'm some distance from the OP now given that the req is to obtain exactly one lock, whereas I don't actually want any of them immediately. The reason I posted is that in my view it was an application of @usr's insightful technique, but i can see it's a stretch now you've pointed it out. If more people object to the lack of direct relevance, I'll move it to a self-answered question. – Ruben Bartelink Aug 04 '22 at 07:28
  • FWIW I approve a self-answered question. An answer is most useful when it answers a specific question, that people are searching for. – Theodor Zoulias Aug 04 '22 at 07:33
  • It seems from the overloads that the `TaskContinuationOptions` and `Scheduler` are mutually exclusive - sticking with the TCO in this instance. I wonder what @usr would say if you made that point on the original answer (which makes more sense as this answer will move away in due course...) – Ruben Bartelink Aug 04 '22 at 07:50
  • 1
    You could check out this article: [Task.Run vs Task.Factory.StartNew](https://devblogs.microsoft.com/pfxteam/task-run-vs-task-factory-startnew/). There is an overload that accepts also a cancellation token, where you pass `CancellationToken.None` or `default`. The article is about the `Task.Factory.StartNew`, but the `ContinueWith` is similar. – Theodor Zoulias Aug 04 '22 at 08:08
  • 1
    https://stackoverflow.com/questions/73233208/using-task-whenany-to-await-capacity-on-a-semaphoreslim/73233209#73233209 – Ruben Bartelink Aug 04 '22 at 09:11
  • In the unlikely event that someone that took the code before now sees this, there was a critical missing `TaskContinuationOptions.OnlyOnRanToCompletion ` in an earlier version of the `AwaitButReleaseAsync` helper https://github.com/jet/propulsion/pull/230 – Ruben Bartelink Aug 20 '23 at 07:27