2

I am trying to rewrite an application from using the BeginXXX methods with AsyncCallbacks to one that uses async/await and the XXXAsync methods. However I am having some trouble with performance. For example here is a snippet of the original code that's used to initialize a bunch of connections:

...
    for (int i = 1; i <= _maxTcpClients; i++) {
        TcpClientState tt = new TcpClientState(new TcpClient(), i);
        try {
            tt.TcpClient.BeginConnect(Host, Port, ConnectCallback, tt);
        } catch (Exception ex) {
            Log.Debug(
                "Error on BeginConnect on RequestHandler (" + tt.HandlerId + ") request (" + tt.RequestId + ")",
                ex);
            CloseRequest(tt);
        }
    }
...
    private static void ConnectCallback(IAsyncResult ar) {
        TcpClientState tt = (TcpClientState)ar.AsyncState;
        Log.Debug("ConnectCallback on TcpClient (" + tt.TcpClientId + ")");

        try {
            tt.TcpClient.EndConnect(ar);
        } catch (Exception ex) {
            Log.Debug("Error on EndConnect on TcpClient (" + tt.TcpClientId + ")", ex);
            CloseRequest(tt);
            Interlocked.Decrement(ref _maxTcpClients);
            return;
        }

        tt.SslStream = new SslStream(tt.TcpClient.GetStream(), false, Helper.ValidateServerCertificate, null);
        try {
            tt.SslStream.BeginAuthenticateAsClient(Host, SslAuthenticateCallback, tt);
        } catch (Exception ex) {
            Log.Debug("Error on BeginAuthenticateAsClient on TcpClient (" + tt.TcpClientId + ")", ex);
            CloseRequest(tt);
            Interlocked.Decrement(ref _maxTcpClients);
        }
    }

I have rewritten this as the following:

...
    for (int i = 1; i <= _maxTcpClients; i++) {
            TcpClientState tt = new TcpClientState(new TcpClient(), i);
            try {
                tt.TcpClient.ConnectAsync(Host, Port).ContinueWith(t => ConnectCallback(tt));
            } catch (Exception ex) {
                Log.Debug("Error on ConnectAsync on TcpClient (" + tt.TcpClientId + ")", ex);
                CloseRequest(tt);
                Interlocked.Decrement(ref _maxTcpClients);
                return;
            }
        }
...
    private static void ConnectCallback(TcpClientState tt) {
        Log.Debug("ConnectCallback on TcpClient (" + tt.TcpClientId + ")");

        tt.SslStream = new SslStream(tt.TcpClient.GetStream(), false, Helper.ValidateServerCertificate, null);
        try {
            tt.SslStream.AuthenticateAsClientAsync(Host).ContinueWith(t => SslAuthenticateCallback(tt));
        } catch (Exception ex) {
            Log.Debug("Error on AuthenticateAsClientAsync on TcpClient (" + tt.TcpClientId + ")", ex);
            CloseRequest(tt);
            Interlocked.Decrement(ref _maxTcpClients);
            return;
        }
    }

There appears to be an enormous performance difference in how quickly the TcpClients are initialized(connection, ssl handshake, and the rest). With the original way of doing it, I can loop through and initialize 100 connections in a few seconds. After rewriting it, it can take updwards of 30 seconds to accomplish the same thing. I can see in the logs that the various callback functions are being executed asynchronously, but everything just takes... longer. I am not sure what I am doing wrong?

Also I know the try catch around the Async methods won't do anything in this case, but that's not the issue right now.

To give an example of the speed difference, here is a debug log snippet when looping 10 times for the original code:

2014-02-25 22:37:06,076 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (1)
2014-02-25 22:37:06,076 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (3)
2014-02-25 22:37:06,077 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (6)
2014-02-25 22:37:06,077 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (10)
2014-02-25 22:37:06,077 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (7)
2014-02-25 22:37:06,077 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (4)
2014-02-25 22:37:06,077 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (8)
2014-02-25 22:37:06,078 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (9)
2014-02-25 22:37:06,079 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (5)
2014-02-25 22:37:06,082 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (2)

And the Async version:

2014-02-25 22:37:51,569 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (1)
2014-02-25 22:37:51,583 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (2)
2014-02-25 22:37:51,936 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (5)
2014-02-25 22:37:51,969 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (3)
2014-02-25 22:37:52,133 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (4)
2014-02-25 22:37:52,311 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (6)
2014-02-25 22:37:52,382 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (8)
2014-02-25 22:37:52,452 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (9)
2014-02-25 22:37:52,466 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (7)
2014-02-25 22:37:52,856 [] DEBUG CPT.Client (null) - ConnectCallback on TcpClient (10)
RadAway
  • 33
  • 5
  • Why `ContinueWith` rather than `async/await`, are you limited to VS2010? If you need to target .NET 4.0 but have an option to develop with VS2012+, you can use [`Microsoft.Bcl.Async`](http://www.nuget.org/packages/microsoft.bcl.async) and still have `async/await`. – noseratio Feb 26 '14 at 01:12
  • If you stick with `ContinueWith`, try `ExecuteSynchronously`, e.g: `ConnectAsync(Host, Port).ContinueWith(t => ConnectCallback(tt), TaskContinuationOptions.ExecuteSynchronously)`. – noseratio Feb 26 '14 at 01:19
  • No I am using VS2012 and targeting .NET 4.5. I can't use await because this is in a for loop. – RadAway Feb 26 '14 at 02:37
  • You still can use `await`, I'll show how. What's the execution environment of this: a WinForms/WPF app, console, service, ASP.NET, etc? – noseratio Feb 26 '14 at 02:44
  • Much appreciated, the environment is WinForms – RadAway Feb 26 '14 at 03:10

1 Answers1

0

Try the following, see if it compares to your BeginXXX/EndXXX version benchmarks.

TcpClientState[] ConnectAll(string host, int port)
{
    var states = new List<TcpClientState>();

    for (int i = 1; i <= _maxTcpClients; i++)
    {
        TcpClientState tt = new TcpClientState(new TcpClient(), i);

        Func<Task> connectAsync = async () =>
        {
            try
            {
                // note ConfigureAwait(false)
                await tt.TcpClient.ConnectAsync(host, port).ConfigureAwait(false);
                tt.SslStream = new SslStream(tt.TcpClient.GetStream(), false, Helper.ValidateServerCertificate, null);
                await tt.SslStream.AuthenticateAsClientAsync(host);

                // move here the code from SslAuthenticateCallback
                // and so on ...  
            }
            catch (Exception ex)
            {
                // you really want to do --_maxTcpClients ?
                Interlocked.Decrement(ref _maxTcpClients);

                Debug.Print(ex.ToString());
                throw; // re-throw or handle
            }
        };

        tt.ConnectionTask = connectAsync();
        states.Add(tt);
    }

    return states.ToArray();
}
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • Unfortunately that did not help, still getting slow results. – RadAway Feb 26 '14 at 07:06
  • @RadAway, that's strange. I believe `Async` socket APIs are just wrappers around BeginXXX/EndXXX APIs. You could try doing the same with `Task.FromAsync`, like [this](http://stackoverflow.com/a/21498956/1768303). Then it really should perform the same as `BeginXXX/EndXXX`. – noseratio Feb 26 '14 at 07:11
  • Unfortunately that did not help much, still getting slow results. I took your example and wrapped the code inside the for loop into an anonymous async function. The only thing I am doing inside the function is "await tt.TcpClient.ConnectAsync(host, port).ConfigureAwait(false);" as you suggested, and the next line just writes an entry to the log. Looping through 10 took just under 1 second, vs the original which took 16 milliseconds. – RadAway Feb 26 '14 at 07:12
  • @RadAway, I might blame DNS resolution which may take place synchronously inside `ConnectAsync`. But this would not explain why `Task.FromAsync` would work differently from Begin/EndXXX. To take it further, you'd need to create something reproducible outside your system. – noseratio Feb 26 '14 at 07:38
  • 2
    I think the problem lies somewhere between the keyboard and the chair... In app.config I had enabled system.diagnostics tracing for sockets for this version of the code. Disabling it fixed the performance of course. Thank you for all your help anyway it has provided me with some new knowledge. – RadAway Feb 26 '14 at 17:27