【问题标题】:How to to make UdpClient.ReceiveAsync() cancelable?如何使 UdpClient.ReceiveAsync() 可取消?
【发布时间】:2021-12-23 21:06:25
【问题描述】:

我有一个带有方法的接口INetwork

Task<bool> SendAsync(string messageToSend, CancellationToken ct)

接口的一个实现有这样的代码:

public async Task<bool> SendAsync(string messageToSend, CancellationToken ct)
{
  var udpClient = new UdpClient();
  var data = Encoding.UTF8.GetBytes (messageToSend);
  var sentBytes = await udpClient.SendAsync(data);
  return sentBytes == data.Length; 
}

很遗憾,UdpClient 类的 SendAsync() 不接受 CancellationToken

所以我开始把它改成:

public Task<bool> SendAsync(string messageToSend, CancellationToken ct)
{
  var udpClient = new UdpClient();
  var data = Encoding.UTF8.GetBytes (messageToSend);
  var sendTask = udpClient.SendAsync(data);
  sendTask.Wait(ct);

  if(sendTask.Status == RanToCompletion)
  {
    return sendTask.Result == data.Length;
  }
}

显然这不起作用,因为没有返回 Task。但是,如果我返回任务,签名将不再匹配。 SendAsync() 返回一个Task&lt;int&gt;,但我需要一个Task&lt;bool&gt;

现在我很困惑。 :-) 如何解决?

【问题讨论】:

  • 在它的默认模式下,我认为sendTask.Result == data.Length 永远不会是假的,而data.Length != messageToSend.Length 唯一一次是当您使用使用超过一个字节来表示的字符时。
  • @ScottChamberlain 可能是。但改变不了问题。如果接口想要一个 bool 来指示发送成功,如何从 Task 获取 Task
  • 只需从SendAsync返回任务,当你等待它时,如果它失败了它会抛出一个异常。
  • @ScottChamberlain 我无法从 SendAsync() 返回任务。它是 Task 但接口中方法的签名需要 Task.

标签: c# asynchronous async-await


【解决方案1】:

我知道这有点晚了,但我最近不得不取消 UdpClient ReceiveAsync/SendAsync。

您的第一个代码块正在发送而没有取消(您的标题说顺便接收...)。

您的第二个代码块绝对不是这样做的方法。您正在调用 *Async,然后是 Task.Wait,它会阻塞直到调用完成。这使得调用有效地同步并且调用 *Async 版本没有意义。最好的解决方案是按如下方式使用 Async:

...
var sendTask = udpClient.SendAsync(data);
var tcs = new TaskCompletionSource<bool>();
using( ct.Register( s => tcs.TrySetResult(true), null) )
{
    if( sendTask != await Task.WhenAny( task, tcs.Task) )
        // ct.Cancel() called
    else
        // sendTask completed first, so .Result will not block
}
...

在 UdpClient 上没有内置的取消方法(没有一个函数接受 CancellationToken),但您可以利用 Task.WhenAny 等待多个任务的能力。这将返回完成的第一个任务(这也是使用Task.Delay() 实现超时的简单方法)。然后我们只需要创建一个在取消CancellationToken 时将完成的任务,我们可以通过创建一个TaskCompletionSource 并使用CancellationToken 的回调设置它来完成。

一旦取消,我们可以关闭套接字以实际“取消”底层的读/写。

最初的想法来自另一个处理文件句柄的 SO 答案,但它也适用于套接字。我通常将它包装在一个扩展方法中,如下所示:

public static class AsyncExtensions
{
    public static async Task<T> WithCancellation<T>( this Task<T> task, CancellationToken cancellationToken )
    {
        var tcs = new TaskCompletionSource<bool>();
        using( cancellationToken.Register( s => ( (TaskCompletionSource<bool>)s ).TrySetResult( true ), tcs ) )
        {
            if( task != await Task.WhenAny( task, tcs.Task ) )
            {
                throw new OperationCanceledException( cancellationToken );
            }
        }

        return task.Result;
    }
}

然后像这样使用它:

try
{
    var data = await client.ReceiveAsync().WithCancellation(cts.Token);
    await client.SendAsync(data.Buffer, data.Buffer.Length, toep).WithCancellation(cts.Token);
}
catch(OperationCanceledException)
{
    client.Close();
}

【讨论】:

  • 这个解决方案发布已经有几年了 - 有什么改进或变化吗?
  • 我使用 C# 已经有几年了,所以我什么都不知道。不过我会感到惊讶,因为这种类型的方法通常用于异步库中(在函数之外处理超时)。
  • 点赞;令人惊讶的是 1) 即使在发布System.IO.Pipelines 之后,微软也没有解决这个问题; 2)这种解决方法完美无缺。非常感谢(如果有兴趣,我将其转换为 F#)
  • @bj0 +1 感谢您的广泛回答!它没有留下任何未解决的问题,除了: ReceiveAsync() 首先有什么用处?您写道:“一旦取消,我们可以关闭套接字以实际“取消”底层读/写。”但实际上,我期待后者是通过取消任务来完成的。如果不是,那么整个异步读取的意义何在?为什么要经历所有的麻烦 - 只需调用同步读取方法并关闭套接字即可取消它。这会抛出一个 SocketException,但我可以抓住它。这不是更容易吗?
  • 此解决方案存在潜在问题。即使在发出取消令牌信号后,SendAsync 任务也会继续运行。如果程序设计为响应取消而关闭,这可能没问题。但是如果程序继续运行,一个孤立的SendAsync 任务将继续运行直到它完成。带有取消令牌参数的 API 通常不会以这种方式工作。
【解决方案2】:

首先,如果你想返回Task&lt;bool&gt;,你可以简单地使用Task.FromResult()。但你可能不应该这样做,拥有一个实际上是同步的异步方法没有多大意义。

除此之外,我还认为您不应该假装该方法已被取消,即使它没有。你可以做的是在你开始真正的SendAsync()之前检查令牌,但就是这样。

如果你真的想尽快假装方法被取消,你可以使用ContinueWith() with cancel:

var sentBytes = await sendTask.ContinueWith(t => t.Result, ct);

【讨论】:

  • 我不明白。 SendAsync() 是异步的,但不能取消。 Send() 是同步的,可以取消……有时候微软的 API 看起来有点奇怪。
  • @Krumelur 你如何取消Send()?我看不出有什么办法。
  • -1 来自我。 ContinueWith 只会在 ReceiveAsync 完成后调用。如果没有数据到达怎么办?该应用程序甚至不会关闭(就我而言)!
  • @uTILLIty 如果取消ct 令牌,那么ContinueWith 返回的Task 将立即取消。但就像我说的,除非必须,否则不应使用该代码。
【解决方案3】:

我建议您向自己发送消息。 一条消息说:“停止”。

【讨论】:

  • 您的答案可以通过额外的支持信息得到改进。请edit 添加更多详细信息,例如引用或文档,以便其他人可以确认您的答案是正确的。你可以找到更多关于如何写好答案的信息in the help center
猜你喜欢
  • 2012-09-20
  • 2017-08-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多