16

I have an interface INetwork with a method:

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

One implementation of the interface has code like this:

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

Unfortunately, SendAsync() of the UdpClient class does not accept a CancellationToken.

So I started changing it to:

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

Obviously this won't work because there is no Task being returned. However if I return the Task, the signatures don't match anymore. SendAsync() returns a Task<int>, but I need a Task<bool>.

And now I'm confused. :-) How to resolve this?

Krumelur
  • 32,180
  • 27
  • 124
  • 263
  • In it's default mode I don't think `sendTask.Result == data.Length` will ever be false, and the only time `data.Length != messageToSend.Length` is when you use characters that use more than one byte to be represented. – Scott Chamberlain Oct 16 '13 at 14:25
  • @ScottChamberlain Could be. But doesn't change the problem. How to get a Task from the Task if the interface wants a bool to indicate that sending succeeded. – Krumelur Oct 16 '13 at 14:31
  • Just return the task from `SendAsync`, when you await on it if it failed it will throw an exception. – Scott Chamberlain Oct 16 '13 at 14:38
  • @ScottChamberlain I cannot return the Task from SendAsync(). It is Task but the signature of the method in the interface wants Task. – Krumelur Oct 16 '13 at 14:39
  • possible duplicate of [Async network operations never finish](http://stackoverflow.com/questions/21468137/async-network-operations-never-finish) – i3arnon Jun 10 '14 at 23:15

3 Answers3

25

I know this is a little late, but I just recently had to make a UdpClient ReceiveAsync/SendAsync cancellable.

Your first code block is sending without a cancel (your title says receive by the way...).

Your second code block is defintely not the way to do it. You are calling *Async, and then Task.Wait, which blocks until the call is complete. This makes the call effectively synchronous and there's no point in calling the *Async version. The best solution is to use Async as follows:

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

There's no built-in way to cancel on UdpClient (none of the functions accept a CancellationToken), but you can take advantage of the ability to await multiple tasks with Task.WhenAny. This will return with the first task that completes (this is also an easy way to use Task.Delay() to implement timeouts). We then just need to create a Task that will complete when the CancellationToken is canceled, which we can do by creating a TaskCompletionSource and setting it with the CancellationToken's callback.

Once canceled, we can close the socket to actually "cancel" the underlying read/write.

The original idea for this came from another SO answer dealing with file handles, but it works with sockets too. I generally wrap it up in an extension method like 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;
    }
}

Then use it like so:

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();
}
bj0
  • 7,893
  • 5
  • 38
  • 49
  • It's been a few years since this solution was posted - any improvements or changes? – khargoosh Mar 01 '18 at 01:32
  • It's been a few years since I used C# so I wouldn't know of any. I would be surprised though, as this type of method is commonly used in async libraries (handle timeout outside of the function). – bj0 Mar 02 '18 at 19:11
  • upvoting; it's surprising that 1) Microsoft hasn't fixed this yet, even after releasing `System.IO.Pipelines`; 2) this workaround works flawlessly. Thanks so much (I converted it to F# if there's any interest) – knocte Mar 21 '19 at 07:27
  • @bj0 +1 Thanks for the great extensive answer! It doesn't leave any questions unanswered, except for: What's ReceiveAsync() good for in the first place? You write: "Once canceled, we can close the socket to actually "cancel" the underlying read/write." But actually, I was expecting that exactly the latter is done by canceling the task. If not, then what's the point in the whole asynchronous read thing? Why go through all the hassle - just call the synchronous read method and close the socket in order to cancel it. This will throw a SocketException, but I can catch it. Isn't this much easier? – SBS Apr 29 '19 at 17:58
  • @SBS if you're working with coroutines (async), the last thing you want to do is hang execution with a blocking call, as it prevents any other coroutines from running. Doing it asynchronously lets you do other work while you wait for the network call to finish. – bj0 Apr 30 '19 at 18:08
  • @bj0 I'm doing the Receive calls in a worker thread, so blocking is not an issue. The problem is to exit the worker thread gracefully while the UdpClient is listening on a port. I don't want to close the socket of the UdpClient, because this renders the UdpClient instance unusable. Currently I'm experimenting with socket timeouts, that is, I set UdpClient.Client.ReceiveTimeout to 500 or so and call Receive(), which blocks temporarily and throws a SocketException if the timeout expires without receiving anything. I can catch this exception and resume listening if no stop signal has arrived. – SBS May 03 '19 at 09:01
  • 4
    This solution has a potential problem. The `SendAsync` task keeps running even after the cancellation token has been signalled. This might be okay if the program is designed to shut down in response to the cancellation. But if the program keeps running, an orphaned `SendAsync` task will continue to run until it completes. APIs with cancellation token parameters do not typically work this way. – Beevik Jun 05 '20 at 15:54
1

First of all, if you want to return Task<bool>, you can simply do that by using Task.FromResult(). But you probably shouldn't do that, it doesn't make much sense to have an async method that's actually synchronous.

Apart from that, I also think you shouldn't pretend that the method was canceled, even if it wasn't. What you can do is to check the token before you start the real SendAsync(), but that's it.

If you really want to pretend that the method was cancelled as soon as possible, you could use ContinueWith() with cancellation:

var sentBytes = await sendTask.ContinueWith(t => t.Result, ct);
svick
  • 236,525
  • 50
  • 385
  • 514
  • 1
    I don't get it. SendAsync() is async but cannot be cancelled. Send() is synchronous and can be cancelled...sometime Microsoft's APIs are a bit weird it seems. – Krumelur Oct 16 '13 at 15:59
  • @Krumelur How do you cancel `Send()`? I don't see any way to do that. – svick Oct 17 '13 at 15:46
  • 1
    -1 from me. ContinueWith will only be called after ReceiveAsync has completed. What if no data ever arrives? The app will not even shutdown (so in my case)! – uTILLIty Jun 20 '18 at 13:18
  • @uTILLIty If you cancel the `ct` token, then the `Task` returned by `ContinueWith` will be immediately cancelled. But like I said, you shouldn't use that code unless you have to. – svick Jun 20 '18 at 15:04
0

Fisrt: You can't "cancel" the UdpClient.ReceiveAsync() directly, but you can simply ignore it after waiting for some time or the time you want cancel, but if you have anxiety about this "infinity wait task thread", you can:

Let's analyze the usage scenario:

  1. Won't reuse the UdpClient instance:
    Just using it, the ReceiveAsync will be shutdown after disposed.
using (var udpClient = new UdpClient())
{
    await udpClient.SendAsync(sendData, sendData.Length, remoteEndPoint);
    var receiveAsyncTask = udpClient.ReceiveAsync();
    // wait time or use CancellationToken
    if (receiveAsyncTask.Wait(1000))
    {
        var data = receiveAsyncTask.Result;
        ProcessResult(data);
    }
}
  1. Will reuse the UdpClient instance:
    Don't worry. When you "Send data" will shutdown the previously ReceiveAsync task.
await udpClient.SendAsync(sendData, sendData.Length, remoteEndPoint);
var receiveAsyncTask = udpClient.ReceiveAsync();
// wait time or use CancellationToken
if (receiveAsyncTask.Wait(1000))
{
    var data = receiveAsyncTask.Result;
    ProcessResult(data);
}
  1. "I want it shutdown immediately!"
    The (2.) tells you "Send data" will shutdown the previously ReceiveAsync task. So just send a new Empty message.
await udpClient.SendAsync(sendData, sendData.Length, remoteEndPoint);
var receiveAsyncTask = udpClient.ReceiveAsync();
// wait time or use CancellationToken
if (receiveAsyncTask.Wait(1000))
{
    var data = receiveAsyncTask.Result;
    ProcessResult(data);
}
else
{
    udpClient.Send(new byte[0], 0, "127.0.0.1", 1);
    await receiveAsyncTask.ContinueWith(task => task.Dispose());
}