Async Methods in .NET

Yesterday I had to rewrite some C# code at work. We were trying to send a message to a webserver without waiting for a response, but for some reason it would hang after only sending a couple messages. I ended up rewriting it to use asynchronous sockets instead of the WebRequest class.

After I had it working, I realized that while the Begin/End pattern that .NET uses for asynchronous operations is better than not having any asychronous ability at all, it would be even better if it had some kind of syntactic support for this, instead of using callback methods.

Here's the code I ended up writing, using callbacks:

    public void log (LogEntry entry) {
        string logString = logEntryToString(entry);
        try {
            Socket s = new Socket(AddressFamily.InterNetwork,
                SocketType.Stream, ProtocolType.Tcp);
            IPAddress ipAddr = Dns.Resolve(m_host).AddressList[0];
            s.BeginConnect(new IPEndPoint(ipAddr, m_port),
                new AsyncCallback(this.finishAsyncLog),
                new Pair(s, logString));
        }
        catch (Exception e) {
            throw new LogException("Problem connecting to log server: "+
                e.Message + " (Log entry: " + logString + ")");
        }
    }

    private void finishAsyncLog(IAsyncResult result) {
        Pair state = (Pair)result.AsyncState;
        Socket s = (Socket)state.First;
        string logString = (string)state.Second;

        try {
            s.EndConnect(result);

            string encodedString = "/log?pairs=" +
                System.Web.HttpUtility.UrlEncode(logString,
                new UTF8Encoding(false));

            string httpRequest = "GET /log?" + logString + "\r\n\r\n";
            Byte[] ByteGet = Encoding.ASCII.GetBytes(httpRequest);
            s.Send(ByteGet, ByteGet.Length, SocketFlags.None);

            s.Close();
        }
        catch (Exception e) {
            Console.WriteLine("Problem connecting to log server: "+
                e.Message + " (Log entry: " + logString + ")");
        }
    }

Notice how I had to break the log() method into two pieces -- one to start the asynchronous operation, and one to finish it. I also had to bundle up a couple local variables into a Pair object, so that I could pass both of them to the callback method (which only takes one state object). If C# and .NET had been designed with some kind of fork or coroutine or continuation facility, the code might have looked more like this:

    public void log (LogEntry entry) {
        string logString = logEntryToString(entry);
        try {
            Socket s = new Socket(AddressFamily.InterNetwork,
                SocketType.Stream, ProtocolType.Tcp);
            IPAddress ipAddr = Dns.Resolve(m_host).AddressList[0];
            IPEndPoint ep = new IPEndPoint(ipAddr, m_port);
            callAsync(s.Connect(ep)) {
                string encodedString = "/log?pairs=" +
                    System.Web.HttpUtility.UrlEncode(logString,
                    new UTF8Encoding(false));
                string httpRequest = "GET " + encodedString + "\r\n\r\n";
                Byte[] ByteGet = Encoding.ASCII.GetBytes(httpRequest);
                s.Send(ByteGet, ByteGet.Length, SocketFlags.None);

                s.Close();
            }
        }
        catch (Exception e) {
            throw new LogException("Problem connecting to log server: "+
                e.Message + " (Log entry: " + logString + ")");
        }
    }

This would be much cleaner -- it's only one method instead of two and I don't have to do any games to package up my local variables. But it does have the drawback of not making it clear that after the socket is connected, the code will be running in a different thread than it started in. It's also not clear whether the catch should apply to the asynchronous code or not. So I suppose it was the right decision for the designers of .NET to use callbacks, but it's still unfortunate that we couldn't have had a more sophisticated solution.

Posted on August 28, 2003 10:50 AM
More languages articles

Comments

Seems like a situation where you'd could use anonymous local functions (lexical closures?)

I don't know about C#, but on a couple of occasions I have ended up using local classes or functor objects in C++ to the same effect.

// Pseudo-C++ code
void  MyClass::log(LogEntry entry)
{
    // Local class for the async operation
    struct CallAsync
    {
        CallAsync(Socket s, string logString)
            : m_s(s), m_logString(logString)
        {
        }
        //
        // Overloaded function operator that Socket.AsyncConnect can call directly
        //
        void operator()()
        {
            string encodedString = "/log?pairs="  + encode(m_logString);
            string httpRequest = "GET  "  +  encodedString  +  "\r\n\r\n"; 
            m_s.Send(httpRequest,  httpRequest.getLength, SocketFlags.None);  
            m_s.Close(); 
        }
    };
    try
    { 
        Socket  s  =  new  Socket(...); 
        IPAddress ipAddr  =  Dns.Resolve(m_host).AddressList[0]; 
        IPEndPoint ep = new IPEndPoint(ipAddr, m_port); 
        //
        // Create functor        
        //
        CallAsync callAsync(s, logString);
        s.AsyncConnect(ep, callAsync);
        //
        // Or simply:
        // 
        //   s.AsyncConnect(CallAsync(s, logString));
        //
    } 
    catch(...) 
    { 
        ...
    } 
}


You can have all your related code together this way, and you don't have to resort to using a Pair to package parameters, but it is still cumbersome. As you mentioned, the scope of the exception handler is a problem though. Maybe CallAsync could have its own exception handler...

There's also the extra work of creating a whole class, writing a constructor and so on. It gets worse if all you want to do is encapsulate a simple inline expression -- don't you wish you could do the following in C...!

    int items[100];    
    qsort(items, 100, compare(a, b) { a - b } );

-K

Posted by: Kaushik at August 28, 2003 03:19 PM

You're right, closures are completely sufficient to get what I want here. I should have realized that. So I guess I'm just complaining that C# requires this clunky delegate syntax instead of having lexical closures.

Posted by: kim at August 28, 2003 03:29 PM

But without continuations, the exception ambiguity remains even with closures. Or am I missing something?

Posted by: Bill Glover at August 28, 2003 04:50 PM

Continuations would resolve the exception ambiguity, but at the cost of making the code return to its caller multiple times. Which in this case is definitely not something I want to happen. I guess I could put a Thread.exit() at the end of the handler though.

Posted by: kim at August 28, 2003 05:29 PM

Closures will be in the next version of C#. They will also include a special Iterator construct which are similar to coroutines.

http://www.gotdotnet.com/team/csharp/learn/Future/VCS%20Language%20Changes.aspx

Posted by: Björn Morén at October 12, 2003 01:56 PM

Why did you even bother posting an article that doesn't fully describe the objects utilized??????????????????????????????????????????????????????????????????????????

Posted by: Joe at March 27, 2007 05:52 PM
Post a comment









Remember info?




Prove you're human. Type "human":