【问题标题】:How to cancel custom awaitable如何取消自定义等待
【发布时间】:2019-12-06 09:09:21
【问题描述】:

我已阅读 Stephen Toub's blog 关于为 SocketAsyncEventArgs 制作自定义等待的内容。这一切正常。但我需要的是一个可取消的等待对象,并且该博客不涉及这个主题。不幸的是,Stephen Cleary 也没有在他的书中介绍如何取消不支持取消的异步方法。 我尝试自己实现它,但我因 TaskCompletionSource 和 Task.WhenAny 失败,因为使用可等待的我实际上并没有等待任务。 这就是我想要的:能够使用带有 TAP 格式的 Socket 的 ConnectAsync 并能够取消它,同时仍然具有 SocketAsyncEventArgs 的可重用性。

public async Task ConnectAsync(SocketAsyncEventArgs args, CancellationToken ct) {}

这就是我到目前为止从 Stephen Toub 的博客中获得的内容(以及 SocketAwaitable 实现):

public static SocketAwaitable ConnectAsync(this Socket socket,
    SocketAwaitable awaitable)
{
    awaitable.Reset();
    if (!socket.ConnectAsync(awaitable.m_eventArgs))
        awaitable.m_wasCompleted = true;
    return awaitable;
}

我只是不知道如何将其转换为 TAP 格式并使其可取消。 任何帮助表示赞赏。

编辑 1: 这是我如何正常取消的示例:

private static async Task<bool> ConnectAsync(SocketAsyncEventArgs args, CancellationToken cancellationToken)
{
    var taskCompletionSource = new TaskCompletionSource<bool>();

    cancellationToken.Register(() =>
    {TaskCompletionSource.Task
        taskCompletionSource.TrySetCanceled();
    });

    // This extension method of Socket not implemented yet
    var task = _socket.ConnectAsync(args);

    var completedTask = await Task.WhenAny(task, taskCompletionSource.Task);

    return await completedTask;
}

【问题讨论】:

  • 你试过检查这个thread吗?

标签: c# sockets async-await cancellation socketasynceventargs


【解决方案1】:

SocketAsyncEventArgs 的自定义可等待对象。

这是可行的,但SocketAsyncEventArgs 专门用于对性能极为敏感的场景。绝大多数(我的意思是 >99.9%)项目不需要它,可以使用 TAP 代替。 TAP 很好,因为它可以与其他技术很好地互操作……比如取消。

我需要的是一个可取消的等待对象,并且博客不涉及这个主题。不幸的是,Stephen Cleary 也没有在他的书中介绍如何取消不支持取消的异步方法。

所以,这里有几件事。从“如何取消不支持取消的异步方法”开始:简短的回答是你不能。这是因为取消是合作的;所以如果一方不能合作,那就不可能了。更长的答案是它是可能的,但通常比它值得的麻烦更多。

在“取消不可取消的方法”的一般情况下,您可以在单独的线程上同步运行该方法,然后中止该线程;这是最有效和最危险的方法,因为中止的线程很容易破坏应用程序状态。为了避免这种严重的损坏问题,最可靠的方法是在单独的进程中运行该方法,该进程可以干净地终止。但是,这通常是矫枉过正,而且麻烦多于其价值。

足够的一般性答案;特别是对于套接字,您不能取消单个操作,因为这会使套接字处于未知状态。以您问题中的Connect 为例:连接和取消之间存在竞争条件。您的代码在请求取消后可能会以连接的套接字结束,并且您不希望该资源保留。所以取消套接字连接的正常和公认的方法是关闭套接字。

任何其他套接字操作也是如此:如果您需要中止读取或写入,那么当您的代码发出取消时,它无法知道读取/写入完成了多少 - 这将离开套接字关于您正在使用的协议处于未知状态。 The proper way to "cancel" any socket operation is to close the socket.

请注意,此“取消”的范围与我们通常看到的取消类型不同。 CancellationToken和朋友代表取消单次操作;关闭套接字会取消该套接字的 所有 操作。因此,套接字 API 不采用 CancellationToken 参数。

遗憾的是,Stephen Cleary 没有在他的书中介绍如何取消不支持取消的异步方法。我尝试自己实现它

“我如何取消不可取消的方法”的问题几乎从来没有通过“使用此代码块”得到妥善解决。具体来说,您使用的方法取消了 await,而不是 操作。请求取消后,您的应用程序仍在尝试连接到服务器,最终可能成功或失败(两种结果都将被忽略)。

在我的 AsyncEx 库中,我确实有一个 coupleWaitAsync 重载,它允许您执行以下操作:

private static async Task MyMethodAsync(CancellationToken cancellationToken)
{
  var connectTask = _socket.ConnectAsync(_host);
  await connectTask.WaitAsync(cancellationToken);
  ...
}

我更喜欢这种API,因为很明显是异步等待被取消了,不是连接操作。

我使用 TaskCompletionSource 和 Task.WhenAny 失败,因为使用 awaitable 我实际上并没有在等待任务。

好吧,要执行类似这样更复杂的操作,您需要将 awaitable 转换为 Task。可能有一个更有效的选择,但这真的很麻烦。即使你弄清楚了所有细节,你仍然会得到“假取消”:一个 看起来 像取消操作但实际上没有的 API - 它只是取消了等待.

这就是我想要的:能够使用带有 TAP 格式的 Socket 的 ConnectAsync 并能够取消它,同时仍然具有 SocketAsyncEventArgs 的可重用性。

TL;DR:您应该关闭套接字而不是取消连接操作。就快速回收资源而言,这是最干净的方法。

【讨论】:

  • 哇哦。感谢您花时间向我解释这一点。正如你所说,我认为在我的情况下实现这一点的努力是不合理的。但是这个话题还是很有趣的。可惜没有完美的解决方案,但我会仔细看看你的图书馆。但就目前而言,我想我只会用 TaskCompletionSource 包装 *Async 方法。据我了解,唯一的缺点(没有自定义等待)是 EventArgs 不可重用,对吗?
  • @Josh:正确; Task 不能重复使用。 EventArgs 试图让它们可重用,这是一个很好的方法,但如果你使用 .NET Core,它几乎会被 ValueTask 取代。
  • 感谢您提及 ValueTask。我没有注意到它们。我刚刚从 Stephen Toub 找到这篇文章:devblogs.microsoft.com/dotnet/…,他提到了 SendAsync 和 ReceiveAsync 与 ValueTask 一起使用的重载。但是我没有在 Socket 文档中看到它们。它们可用于 .Net Core 3 吗?我想这将是考虑对象分配时的最佳选择。但无论如何。我的需求不能证明使用 ValueTask 是合理的。
  • @Josh:他们是extension methods。请注意,一些重载返回 Task&lt;T&gt;,而其他重载返回 ValueTask&lt;T&gt;
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-13
  • 2019-10-19
  • 2020-04-23
相关资源
最近更新 更多