Does C# AsyncCallback creates a new thread?

2019-03-18 06:20发布

问题:

I have written an HttpListener which listens on one of the ports:

httpListener.BeginGetContext(new AsyncCallback(ListenerCallback), httpListener);

The ListenerCallback handles any request that is received on the listener uri. If exception occurs during handling request, it runs a diagnostics routine, which tries to hit listener uri just to check if the listener is in fact alive and listening on the uri and writes the log of response returned by the listener. Listener simply returns string Listening... to such dummy requests.

Now during testing, when exception occurred in other modules which resulted in the execution of the diagnostic modules, I can see the listener returned Listening... properly when I checked the logs. However when exception occurred in the ListenerCallback, the attempt to hit the listener URI inside diagnostics threw following exception:

System.Net.WebException : The operation has timed out
   at System.Net.HttpWebRequest.GetResponse()
   at MyPackage.Diagnostics.hitListenerUrl(String url) in c:\SW\MyApp\MyProj\Diagnostics.cs:line 190

That line 190 in diagnostics module is as follows:

189     HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
190     HttpWebResponse response = (HttpWebResponse)request.GetResponse();

Now if AsyncCallback dispatches new thread and run ListenerCallback in that new thread, it must not result Operation Timeout when the dummy request is sent through the diagnostics. This is what I thought the desired behavior should be since it is *Async*Callback. In fact MSDN also says the same:

Use an AsyncCallback delegate to process the results of an asynchronous operation in a separate thread.

But seems that is not the case. Am I missing something here?

Interpreting Visually:

回答1:

It is entirely an implementation detail of the class' BeginXxx() method. There are two basic schemes:

  • BeginXxx() starts a thread to get the job done, that thread makes the callback
  • BeginXxx() asks the operating system to get job done, using an I/O completion port to ask to be notified when it is done. The OS starts a thread to deliver the notification, that runs the callback.

The second approach is very desirable, it scales well with the program being able to have many pending operations. And is the approach used by HttpListener, the TCP/IP driver stack on Windows supports completion ports. Your program can support thousands of sockets easily, important in server scenarios.

The EndXxx() call in the callback reports any mishaps encountered while trying to complete the I/O request by throwing an exception. In your case, the BeginGetContext() requires EndGetContext() in the callback. If you don't catch the exception then your program will terminate.

Your code snippet does not actually demonstrate any asynchronous I/O. You called GetResponse() instead of BeginGetResponse(). No callback is involved at all, it will thus be the GetResponse() method that fails and throws the exception.



回答2:

Does C# AsyncCallback creates a new thread?

No, it does not itself, it is just a callback. However, the code which invokes the callback may be calling it on a thread different from the thread the original operation was started on (in your case, the operation is httpListener.BeginGetContext).

Usually (but not necessary), it is invoked on a random ThreadPool thread which happened to handle the completion of the underlying socket IO operation (for more details, here is a great read: There Is No Thread).

Use an AsyncCallback delegate to process the results of an asynchronous operation in a separate thread.

I believe that means you should process the results of an asynchronous operation on the thread your callback is called on. This thread is almost always separate from the thread which initiated the operation, as explained above. That's also what their sample code apparently does. Note they don't create any new threads inside ProcessDnsInformation.

Once you have received the callback, it is up to you then how to organize the threading model of your server application. The main concern is, it should remain being responsive and scalable. Ideally, you should serve the incoming request on the same thread it arrives on, and release this thread as soon as you've done with any CPU-bound job required to process the request. As a part of the processing logic, you may need to do other IO-bound tasks (access files, execute DB queries, call web services, etc). While doing that, you should be using asynchronous versions of the relevant APIs as mush as possible, to avoid blocking the request thread (again, refer to There Is No Thread).

IMO, with the Asynchronous Programming Model (APM) pattern that you've chosen, implementing such logic might be quite a tedious task (especially, the error handling and recovery part of it).

However, using Task Parallel Library (TPL), async/await pattern and Task-based APIs like HttpListener.GetContextAsync, it's a piece of cake. The best part: you don't have to worry about threading much any more.

To give you an idea of what I'm talking about, here is an example of a low-level TCP server. A very similar concept can be used while implementing an HttpListener-based HTTP server.



回答3:

To add to Hans' excellent answer,

Even when an asynchronous operation is performed, it is actually possible for it to complete synchronously -- even in scheme #2, it is incorrect to say callbacks will come back on another thread. Counting on that behavior might actually end up with you getting deadlocks or overflowing your stack.

You can check the IAsyncResult.CompletedSynchronously property to determine this. When set to true, it is highly likely that the completion callback came in on the same thread that started the async operation: the callback will run during your Begin* call, and must complete before it can return.