Search code examples
c#socketsasync-awaitcancellationsocketasynceventargs

How to cancel custom awaitable


I've read Stephen Toub's blog about making a custom awaitable for SocketAsyncEventArgs. This works all fine. But 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. I tried to implement it myself, but I fail with the TaskCompletionSource and Task.WhenAny because with the awaitable I'm not actually awaiting a task. 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.

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

And this is what I have from Stephen Toub's blog so far (and the SocketAwaitable implementation):

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

I just can't figure out how to get this into the TAP format and making it cancellable. Any help is appreciated.

Edit 1: This is the example how I would do cancellation normally:

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;
}

Solution

  • custom awaitable for SocketAsyncEventArgs.

    This is doable, but SocketAsyncEventArgs is specifically for extremely performance-sensitive scenarios. The vast majority (and I mean >99.9%) of projects do not need it and can use TAP instead. TAP is nice because it interoperates well with other techniques... like cancellation.

    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.

    So, there's a couple of things here. Starting with "how to cancel async methods that don't support cancellation": the short answer is that you can't. This is because cancellation is cooperative; so if one side can't cooperate, it's just not possible. The longer answer is that it's possible but usually more trouble than it's worth.

    In the general case of "cancel an uncancelable method", you can run the method synchronously on a separate thread and then abort that thread; this is the most efficient and most dangerous approach, since aborted threads can easily corrupt application state. To avoid this serious corruption problem, the most reliable approach is to run the method in a separate process, which can be terminated cleanly. However, that's usually overkill and way more trouble than it's worth.

    Enough of the general answer; for sockets specifically, you cannot cancel a single operation, because this leaves the socket in an unknown state. Take the Connect example in your question: there's a race condition between the connection and the cancel. Your code might end up with a connected socket after it requests cancellation, and you don't want that resource sticking around. So the normal and accepted way to cancel a socket connection is to close the socket.

    The same goes for any other socket operation: if you need to abort a read or write, then when your code issues a cancel, it can't know how much of the read/write completed - and that would leave the socket in an unknown state with regard to the protocol you're using. The proper way to "cancel" any socket operation is to close the socket.

    Note that this "cancel" is at a different scope than the kind of cancellation we normally see. CancellationToken and friends represent the cancellation of a single operation; closing a socket cancels all operations for that socket. For this reason, the socket APIs do not take CancellationToken parameters.

    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

    The question "how do I cancel an uncancelable method" is almost never properly solved with "use this code block". Specifically, the approach you used cancels the await, not the operation. After a cancel is requested, your application is still trying to connect to the server, and may eventually succeed or fail (both results would be ignored).

    In my AsyncEx library, I do have a couple of WaitAsync overloads, which allow you to do something like this:

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

    I prefer this kind of API because it's clear that it's the asynchronous wait that is cancelled, not the connection operation.

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

    Well, to do something more complex like this, you'll need to convert that awaitable to a Task. There may be a more efficient option, but that's really getting into the weeds. And even if you figure out all the particulars, you'll still end up with "fake cancelling": an API that looks like it cancels the operation but it really doesn't - it just cancels the await.

    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: You should close the socket instead of cancelling a connection operation. That's the cleanest approach in terms of reclaiming resources quickly.