8

我有几个异步网络操作返回一个可能永远不会完成的任务:

  1. UdpClient.ReceiveAsync不接受CancellationToken
  2. TcpClient.GetStream返回一个NetworkStream不尊重CancellationTokenon 的Stream.ReadAsync(仅在操作开始时检查取消)

两者都等待可能永远不会出现的消息(例如,由于丢包或无响应)。这意味着我有永远不会完成的幻像任务,永远不会运行的延续和暂停使用的套接字。我知道我可以使用TimeoutAfter,但这只会解决延续问题。

那我该怎么办?

4

2 回答 2

10

所以我做了一个扩展方法IDisposable来创建一个CancellationToken在超时时处理连接的方法,所以任务完成并且一切都继续:

public static IDisposable CreateTimeoutScope(this IDisposable disposable, TimeSpan timeSpan)
{
    var cancellationTokenSource = new CancellationTokenSource(timeSpan);
    var cancellationTokenRegistration = cancellationTokenSource.Token.Register(disposable.Dispose);
    return new DisposableScope(
        () =>
        {
            cancellationTokenRegistration.Dispose();
            cancellationTokenSource.Dispose();
            disposable.Dispose();
        });
}

而且用法非常简单:

try
{
    var client = new UdpClient();
    using (client.CreateTimeoutScope(TimeSpan.FromSeconds(2)))
    {
        var result = await client.ReceiveAsync();
        // Handle result
    }
}
catch (ObjectDisposedException)
{
    return null;
}

额外信息:

public sealed class DisposableScope : IDisposable
{
    private readonly Action _closeScopeAction;
    public DisposableScope(Action closeScopeAction)
    {
        _closeScopeAction = closeScopeAction;
    }
    public void Dispose()
    {
        _closeScopeAction();
    }
}
于 2014-01-30T21:49:43.930 回答
3

那我该怎么办?

在这种特殊情况下,我宁愿优雅地使用UdpClient.Client.ReceiveTimeoutTcpClient.ReceiveTimeout超时 UDP 或 TCP 接收操作。我希望超时错误来自套接字,而不是来自任何外部来源。

如果除此之外我还需要观察其他一些取消事件,例如 UI 按钮单击,我会使用WithCancellationStephen Toub 的“如何取消不可取消的异步操作?” , 像这样:

using (var client = new UdpClient())
{
    UdpClient.Client.ReceiveTimeout = 2000;

    var result = await client.ReceiveAsync().WithCancellation(userToken);
    // ...
}

为了解决评论,如果ReceiveTimeout对 没有影响ReceiveAsync,我仍然会使用WithCancellation

using (var client = new UdpClient())
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(userToken))
{
    UdpClient.Client.ReceiveTimeout = 2000;
    cts.CancelAfter(2000);

    var result = await client.ReceiveAsync().WithCancellation(cts.Token);
    // ...
}

IMO,这更清楚地表明了我作为开发人员的意图,并且对第 3 方来说更具可读性。另外,我不需要捕捉ObjectDisposedException异常。我仍然需要观察OperationCanceledException调用此方法的客户端代码中的某个位置,但无论如何我都会这样做。OperationCanceledException通常从其他例外中脱颖而出,我可以选择检查OperationCanceledException.CancellationToken以观察取消的原因。

除此之外,与@I3arnon 的回答没有太大区别。我只是觉得我不需要另一种模式,因为我已经可以使用WithCancellation了。

要进一步解决评论:

  • 我只会OperationCanceledException在客户端代码中捕获,即:

async void Button_Click(sender o, EventArgs args) { try { await DoSocketStuffAsync(_userCancellationToken.Token); } catch (Exception ex) { while (ex is AggregateException) ex = ex.InnerException; if (ex is OperationCanceledException) 返回;// 如果取消则忽略 // 否则报告 MessageBox.Show(ex.Message); } }
  • 是的,我会在WithCancellation每次ReadAsync通话时使用,我喜欢这个事实,原因如下。首先,我可以创建一个扩展ReceiveAsyncWithToken

public static class UdpClientExt
{
    public static Task<UdpReceiveResult> ReceiveAsyncWithToken(
        this UdpClient client, CancellationToken token)
    {
        return client.ReceiveAsync().WithCancellation(token);
    }
}

其次,从现在起的 3 年内,我可能会审查 .NET 6.0 的代码。届时,微软可能会有一个新的 API UdpClient.ReceiveAsyncWithTimeout,. 就我而言,我将简单地将ReceiveAsyncWithToken(token)orReceiveAsync().WithCancellation(token)替换为ReceiveAsyncWithTimeout(timeout, userToken). 处理起来不会那么明显CreateTimeoutScope

于 2014-01-31T00:10:16.070 回答