3

All solutions I found so far are based on WaitOne: How to configure socket connect timeout or spawning a worker thread

For me, blocking the thread with WaitOne defeats the purpose of async methods. Spawning another thread is not much better (as async model strives to use as less threads as possible).

Is there any other solution which would still let me abort the connection attempt without blocking the current thread or spawning another one?

I'm developing a library used by external code which has no knowledge of what's going on inside my library (the code just calls my ConnectAsync method and I do the rest including TcpClient.ConnectAsync and so on). The external code can be anything (web app, desktop app, Windows service, whatever). Ideally, the solution should not require the external code to do anything to abort the operation beyond setting .Timeout property of my class. However, if it's the only the way to avoid blocks or worker threads when implementing custom connection timeout, I'd be grateful to see which options I have (in terms of async/await model).

Community
  • 1
  • 1
Alex
  • 2,469
  • 3
  • 28
  • 61
  • I think my question here ([Async network operations never finish](http://stackoverflow.com/questions/21468137/async-network-operations-never-finish)) is a duplicate. But I don't want to dupe-hammer it. – i3arnon Feb 19 '15 at 17:03

4 Answers4

6

TcpClient.SendAsync doesn't receive a CancellationToken so it can't really be canceled (How do I cancel non-cancelable async operations?). You can use the WithTimeout extensions method:

public static Task<TResult> WithTimeout<TResult>(this Task<TResult> task, TimeSpan timeout)
{
    var timeoutTask = Task.Delay(timeout).ContinueWith(_ => default(TResult), TaskContinuationOptions.ExecuteSynchronously);
    return Task.WhenAny(task, timeoutTask).Unwrap();
}

This doesn't cancel the original operation though, only allows your code to behave as if it did. The abandoned operation will linger forever unless handled explicitly.

To actually cancel the underlying operation you should make sure to call Dispose on the TcpClient (preferably via a using scope). That would make the abandoned task throw an ObjectDisposedException (or others) so be aware of that.

You can take a look at my answer here about using a TimeoutScope:

try
{
    var client = new TcpClient();
    using (client.CreateTimeoutScope(TimeSpan.FromSeconds(2)))
    {
        var result = await client.ConnectAsync();
        // Handle result
    }
}
catch (ObjectDisposedException)
{
    return null;
}
Community
  • 1
  • 1
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • 1
    Thanks, this helps. I cannot do 'using' block as my method only connects (I'm not controlling the entire lifecycle of the connection) but it's not so important here. – Alex Feb 19 '15 at 19:20
  • `ObjectDisposedException` should be `NullReferenceException`. See: https://connect.microsoft.com/VisualStudio/feedback/details/774269/tcpclient-endconnect-throws-null-reference-exception-when-tcpclient-has-been-disposed – antak May 27 '16 at 04:37
  • Instead of calling `TcpClient.Dispose` consider instead `TcpClient.Client.Shutdown`. This shuts down the connection causing the original operation to complete without throwing an exception. The `TcpClient` can then be safely disposed. – AndyMcoy Dec 11 '19 at 15:43
1

If you create a second task for the timeout (Task.Delay does nicely), then you can use Task.WhenAny to complete as soon as either your task, or the timeout completes.

var timeout = Task.Delay(whatever);
var mightTimeout = Task.WhenAny(new {} { timeout, originalTask });

// let mightTimeout complete by whatever method (eg. async)

if (mightTimeout == timeout) {
  // Timeout!!
  // Put abort code in here.
}
Richard
  • 106,783
  • 21
  • 203
  • 265
  • That doesn't abort anything. You may want to throw in a `CancellationToken` somewhere. – Jeroen Mostert Feb 19 '15 at 17:08
  • @JeroenMostert True, but that's not really the interesting bit here. – Richard Feb 19 '15 at 17:11
  • 2
    It becomes really interesting soon enough if you chuck this in production code and then get complaints from people that your application makes the machine run out of ports... the approach in general is usable, but only as long as you keep in mind that nothing has really been aborted, and that you cannot go and start a new operation without considering that. – Jeroen Mostert Feb 19 '15 at 17:12
  • @JeroenMostert I'm not suggesting the port isn't closed. – Richard Feb 19 '15 at 17:13
0

I found a solution using

Await Task.WhenAny

Task.WhenAny will finish whenever any of the task included finishes first. Put it inside an async function

Here's an example that works for me:

Public Async Function TCPConnectionAsync(HostIpAddress, Port) As Task(Of String)
 Dim client As New TcpClient     
  Await Task.WhenAny(client.ConnectAsync(HostIpAddress, Porta), Task.Delay(3000)) 
 'this above will not block because function is async,
 'whenever the connection is successful or Task.Delay expires the task will end
 ' proceeding next, where you can check the connection 


 If client.Connected = False Then 'this will be evaluated after 3000ms
      client.Close()
      return "not connected"
 else
      'do here whatever you need with the client connection


       client.Close()
       return "all done"
 End If
End Sub
qfactor77
  • 810
  • 7
  • 10
0

For those who want to use asynchronous solution with async/await with timeout support:

    public static async Task<bool> PingHostAndPort(string host, int port, int timeout)
    {
        using (var tcpClient = new TcpClient())
        {
            Task connectTask = tcpClient.ConnectAsync(host, port);
            Task timeoutTask = Task.Delay(timeout);
            // Double await is required to catch the exception.
            Task completedTask = await Task.WhenAny(connectTask, timeoutTask);
            try
            {
                await completedTask;
            }
            catch (Exception)
            {
                return false;
            }

            if (timeoutTask.IsCompleted)
            {
                return false;
            }

            return connectTask.Status == TaskStatus.RanToCompletion && tcpClient.Connected;
        };
    }

Note that you can use this logic (utilizing the timeoutTask) from any operations/methods that miss a timeout parameter.

Mustafa Özçetin
  • 1,893
  • 1
  • 14
  • 16