等待 2 个异步方法中的任何一个 return 结果

Waiting for any of 2 async methods to return a result

我想知道我在处理异步编程时是否遵循了最佳实践。我手头的问题是:我同时与 2 台设备通话。我可以使用 SendMessageAsync(msg) 方法向他们发送消息。两台设备同时收到此消息,但在 return 中,只有一台设备发送回复,而另一台设备根本不接听。

此外,该方法应该接受 CancellationToken,例如整个事情可以在超时或其他原因后取消。

所以我写了这个方法来读取消息:

public async Task<Message> GetMessageAsync(CancellationToken token)
{
    using (var cts = CancellationTokenSource.CreateLinkedTokenSource(token, new CancellationToken(false)))           
    {
        var device1 = Task.Run(async () => { return await GetDevice1Async(cts.Token); });
        var device2 = Task.Run(async () => { return await GetDevice2Async(cts.Token); });

        var response = await Task.WhenAny(device1, device2);

        cts.Cancel(); //Only one device answers, so cancel the other other one

        return response.Result;
    }            
}

我想知道我的解决方案是否遵循了最佳实践。具体来说,我有兴趣获得非常好的性能(我与之交谈的设备是 USB 设备,所以我希望能够快速为它们提供服务)。所以每次我需要阅读一条消息时,我都不太乐意创建两个任务。

目前我的解决方案似乎有效,但据报道在某些机器上,它 运行 很慢。然而,在我的机器上,它 运行 相当快,所以我不知道是因为我的代码有问题还是其他原因。

我做的对吗?有没有办法改进这个解决方案?

编辑: 根据Jeroen Mostert和usr的建议,我更新了代码如下:

public async Task<Message> GetMessageAsync(CancellationToken token)
{
    using (var cts = CancellationTokenSource.CreateLinkedTokenSource(token, new CancellationToken(false)))           
    {
        var taskList = new List<Task<Message>> {
            GetDevice1Async(cts.Token),
            GetDevice2Async(cts.Token)
        };

        // wait for any operation to finish, then cancel the other one
        var task = await Task.WhenAny(taskList);
        cts.Cancel();

        //ensure both operations are either finished or cancelled before returning
        try {
            await Task.WhenAll(taskList);
        }
        catch (OperationCanceledException)
        { 
            //The exception is expected as is safe to ignore
        }

        return task.Result;
    }            
}

基本上这样就可以了

但是,有一个问题是您总是放弃一项任务。它被要求取消自己,但如果它不这样做,它将继续 运行。这会累积资源使用量,并且可能会导致您所看到的缓慢。

在 .NET IO 中很少能轻易取消。例如,对于套接字,除了关闭套接字之外,您不能取消 IO。与您的 USB 设备通话时,您需要确保取消确实有效。

另一个问题是 cts 可能在最后一次使用其令牌后被处置。在您调用 cts.Dispose() 时,仍有一项任务 运行ning 可能 将自己注册到令牌中。我不确定这是否保证有效。

您可以通过等待已取消的操作实际取消来解决此问题:

    cts.Cancel();
    await Task.WhenAll(tasks); //Maybe need to swallow exceptions.
    return response.Result;

您可以将 Task.Run(async 简化为正常的方法调用。这会稍微改变语义。它通过同步上下文并同步执行异步方法的某些部分。你可能想要也可能不想要那个。这不是一个重要的效率问题。我的主观评价是,此 Task.Run 模式阐明了代码并使代码更容易正确。