【问题标题】:Task.WhenAny and SemaphoreSlim classTask.WhenAny 和 SemaphoreSlim 类
【发布时间】:2021-03-03 07:49:57
【问题描述】:

当使用WaitHandle.WaitAny and Semaphore class 时,如下所示:

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();

似乎可以保证WaitHandle.WaitAny 只获取一个信号量。

是否有可能为异步(async/await)代码获得类似的行为?

【问题讨论】:

  • 你的意思是Task.WaitAny()
  • 我觉得没关系,因为Task.WaitAny似乎只是Task.WhenAny的同步版本。
  • 我也打算发Task.WhenAny()
  • 那么你应该投票赞成这个问题。
  • @drowa:对于您遇到的任何问题,这几乎肯定是错误的解决方案。您要解决的实际问题是什么?

标签: c# asynchronous


【解决方案1】:

我想不出内置的解决方案。我会这样做:

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());

这会获取两个信号量,但会立即释放第二个信号量。这应该是等价的。我想不出不必要地获取信号量的负面后果(当然性能除外)。

【讨论】:

  • 这是一个好的开始。 ContinueWith 返回的任务的“泄漏”并不好闻。
  • 我喜欢跟踪我创建的任务。例如,确保在终止应用程序开始之前全部完成。
  • 我明白了,我也喜欢这样做。对于错误跟踪特别有用。解决方案:让 WaitAsync 采用 CancellationToken。发出等待中止的令牌。然后,等待继续任务以及等待任务。等待应该立即完成,并且所有任务都已正确关闭。 (我不会添加那个代码。太多了。)
  • 我也在考虑那个解决方案。
  • 我想知道它的同步版本是什么(即仍然使用SemaphoreSlim)。我们可能需要使用SemaphoreSlim.AvailableWaitHandle
【解决方案2】:

这是WaitAnyAsync 方法的通用实现,它异步获取任何提供的信号量:

/// <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
    }
}

随着争用越来越高,这种方法变得越来越低效,所以我不建议在热路径中使用它。

【讨论】:

    猜你喜欢
    • 2022-08-04
    • 1970-01-01
    • 1970-01-01
    • 2017-08-27
    • 2019-10-16
    • 2012-09-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多