C#。取消任务时 SemaphoreSlim.WaitAsync 不会抛出 OperationCanceledException

2 c# task async-await

首先,请原谅我的英语。我会简单地说,在 Task.WhenAny 之后的附加代码中,我预期五个任务中至少有三个会被取消,但所有任务都圆满结束。当任务被取消时,SemaphoreSlim.WaitAsync 不会抛出OperationCanceledException。

class Program
{
    private static CancellationTokenSource methodRequests = new CancellationTokenSource();
    private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);

    static void Main(string[] args)
    {
        int[] delays = new int[] { 5000, 5010, 5020, 5030, 5040 };

        IEnumerable<Task> tasks = from delay in delays select MethodAsync(delay, new CancellationTokenSource().Token);

        Task.WhenAny(tasks).Wait();

        methodRequests.Cancel();

        Console.ReadKey();
    }

    static async Task MethodAsync(int milliseconds, CancellationToken cancellationToken)
    {
        var methodRequest = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, methodRequests.Token);

        try
        {
            await semaphore.WaitAsync(methodRequest.Token);

            Thread.Sleep(milliseconds);

            Console.WriteLine($"Task finished {milliseconds}");
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine($"Task canceled {milliseconds}");
        }
        finally
        {
            semaphore.Release();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我究竟做错了什么?

谢谢。

Pet*_*iho 5

代码中的问题是该MethodAsync()方法在方法完成之前永远不会返回Thread.Sleep()。这意味着每项任务在前一项任务完成之前都不会开始。这是您的代码的一个版本,可以更清楚地说明这一点:

private static CancellationTokenSource methodRequests = new CancellationTokenSource();
private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);

static void Main(string[] args)
{
    int[] delays = new int[] { 5000, 5010, 5020, 5030, 5040 };

    IEnumerable<Task> tasks = from delay in delays select MethodAsync(delay, new CancellationTokenSource().Token);

    Task.WhenAny(tasks).Wait();

    methodRequests.Cancel();

    ReadKey();
}

static async Task MethodAsync(int milliseconds, CancellationToken cancellationToken)
{
    var methodRequest = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, methodRequests.Token);

    try
    {
        WriteLine($"waiting semaphore (will wait {milliseconds} ms)");
        await semaphore.WaitAsync(methodRequest.Token);

        WriteLine($"waiting {milliseconds} ms");
        Thread.Sleep(milliseconds);

        WriteLine($"Task finished {milliseconds}");
    }
    catch (OperationCanceledException)
    {
        WriteLine($"Task canceled {milliseconds}");
    }
    finally
    {
        semaphore.Release();
    }
}
Run Code Online (Sandbox Code Playgroud)

其输出是:

等待信号量(将等待 5000 毫秒)
等待 5000 毫秒
任务完成5000
等待信号量(将等待 5010 毫秒)
等待 5010 毫秒
任务完成5010
等待信号量(将等待 5020 毫秒)
等待 5020 毫秒
任务完成5020
等待信号量(将等待 5030 毫秒)
等待 5030 毫秒
任务完成5030
等待信号量(将等待 5040 毫秒)
等待 5040 毫秒
任务完成5040

正如您所看到的,在上一个任务完成之前,您甚至不会看到“正在等待信号量...”消息。这是因为在该方法返回当前元素的值之前,LINQselect无法继续处理序列中的下一个元素MethodAsync(),并且在完成之前不会发生这种情况Thread.Sleep()

您可能认为 应该await semaphore.WaitAsync()屈服于调用者,允许返回 并继续Task枚举。select但是,只有当信号量不可用时才会发生这种情况。它在每次调用时都可用,因为每次调用仅在前一个调用完成后发生,因为当进行前一个调用时,信号量可用。由于在调用 时信号量可用WaitAsync(),因此await同步完成。即代码直接继续执行Thread.Sleep()而不是屈服于调用者。

最终效果是,WhenAny()直到所有调用Thread.Sleep()(当然semaphore.WaitAsync()还有所有调用)完成后,调用才会发生。

据推测,这出现在某些现实场景中,您发布的代码仅用于演示目的。因此,很难确切地说出应该解决什么问题。但是,在您发布的代码示例中,只需切换到Task.Delay()而不是Thread.Sleep(). 由于延迟始终不为零,因此即使信号量本身可用,该方法也始终会在该点产生结果。这允许在当前呼叫中的“工作”完成之前select继续进行下一个呼叫。MethodAsync()

通过这种方式,所有任务实际上都是按照您最初的预期同时创建的。

无论现实世界的代码是什么样的,您想要做的是确保在获取信号量之后有一个异步操作,以允许该方法实际返回给调用者,以便下一个操作可以也开始吧。