异步网络操作永远不会完成

时间:2014-01-30 21:49:44

标签: c# networking async-await idisposable cancellation-token

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

  1. UdpClient.ReceiveAsync不接受CancellationToken
  2. TcpClient.GetStream返回的NetworkStream不尊重CancellationToken上的Stream.ReadAsync(仅在操作开始时检查取消)
  3. 两者都在等待可能永远不会来的消息(例如因丢包或没有响应)。这意味着我有幻像任务永远不会完成,永远不会运行的延续和使用套接字。我知道我可以使用TimeoutAfter,但这只会解决延续问题。

    那我该怎么办?

2 个答案:

答案 0 :(得分:7)

所以我在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();
    }
}

答案 1 :(得分:3)

  

那我该怎么办?

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

除此之外我还需要观察其他一些取消事件,比如点按用户界面按钮,我只会使用Stephen Toub的"How do I cancel non-cancelable async operations?"中的WithCancellation,如下所示:

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

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

要解决评论,如果ReceiveTimeoutReceiveAsync没有影响,我仍然会使用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,这更清楚地表明了我作为开发人员的意图,并且对第三方更具可读性。另外,我不需要捕获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)
            return; // ignore if cancelled
        // report otherwise
        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的此代码。到那时,Microsoft可能会有一个新的API,UdpClient.ReceiveAsyncWithTimeout。在我的情况下,我只需将ReceiveAsyncWithToken(token)ReceiveAsync().WithCancellation(token)替换为ReceiveAsyncWithTimeout(timeout, userToken)。处理CreateTimeoutScope时不会那么明显。