如何取消自定义等待

How to cancel custom awaitable

我已阅读 Stephen Toub's blog 关于为 SocketAsyncEventArgs 定制可等待的内容。这一切都很好。但我需要的是一个可取消的可等待对象,而该博客并未涵盖该主题。不幸的是,Stephen Cleary 也没有在他的书中介绍如何取消不支持取消的异步方法。 我尝试自己实现它,但我在 TaskCompletionSource 和 Task.WhenAny 上失败了,因为对于 awaitable 我实际上并没有在等待任务。 这就是我想要的:能够以 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;
}

custom awaitable for SocketAsyncEventArgs.

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

what I need is a cancellable awaitable and the blog doesn't cover this topic. Also Stephen Cleary unfortunately doesn't cover in his book how to cancel async methods that don't support cancellation.

所以,这里有几件事。从 "how to cancel async methods that don't support cancellation" 开始:简短的回答是你不能。这是因为取消是合作的;所以如果一方不能合作,那是不可能的。较长的答案是可能的,但通常比它的价值更麻烦。

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

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

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

请注意,此 "cancel" 与我们通常看到的那种取消的范围不同。 CancellationToken和好友代表取消单次操作;关闭套接字会取消该套接字的 all 操作。因此,套接字 APIs 不带 CancellationToken 参数。

Stephen Cleary unfortunately doesn't cover in his book how to cancel async methods that don't support cancellation. I tried to implement it myself

问题 "how do I cancel an uncancelable method" 几乎从未 用 "use this code block" 正确解决。具体来说,您使用的方法取消了 await,而不是 operation。请求取消后,您的应用程序仍在尝试连接到服务器,最终可能成功或失败(两种结果都将被忽略)。

在我的 AsyncEx 库中,我有一个 couple of WaitAsync 重载,它允许你做这样的事情:

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

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

I fail with the TaskCompletionSource and Task.WhenAny because with the awaitable I'm not actually awaiting a task.

好吧,要完成像这样更复杂的事情,您需要将可等待对象转换为 Task。可能有一个更有效的选择,但这真的很麻烦。即使你弄清楚了所有的细节,你仍然会得到 "fake cancelling": 一个 看起来 的 API 就像它取消了操作但实际上并没有't - 它只是取消等待。

This is what I would like to have: being able to use Socket's ConnectAsync with TAP format and being able to cancel it, while still having the reusability of SocketAsyncEventArgs.

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