C#异步调用机制(英转)
注:本文转载自http://www.codeproject.com/KB/cs/AsyncMethodInvocation.aspx,使用本文中的代码请注意遵守licensed :CPOL
仔细阅读本文 可以对C#的异步调用机制从入门到逐步深入的理解,作者从最简单的同步机制开始,使用一个简单例子FOO逐步将其复杂化,并通过实验结果使我们对异步方法调用中的委托、线程池、线程间安全等较深入的问题进行探讨,由浅入深的理解使博主收益匪浅。
Asynchronous Method Invocation
Introduction
In this article, I am going to explain asynchronous method calls and how to use them. After playing with delegates, threads, and asynchronous invocation for so long, it would be a sin not to share some of my wisdom and knowledge on the subject, so hopefully, you won�t be looking at an MSDN article at 1 AM wondering why you decided to go into computers. I will try to use baby steps and lots of examples� Overall, I will cover how to call methods asynchronously, how to pass parameters to such methods, and how to find out when a method completes execution. Finally, I will show the Command Pattern in use for simplifying some of the code. The big advantage with .NET asynchronous method invocation is that you can take any method you have in your project, and you can call it asynchronously without touching the code of your method. Although most of the magic is within the .NET framework, it is important to see what is going on in the back, and that�s what we are going to study here.
Synchronous vs. Asynchronous
Let me try to explain synchronous and asynchronous method invocations with an example, because I know people on The Code Project like to see code and not read War and Peace (not that I have anything against this book).
Synchronous method invocation
Suppose we have a function Foo()
that takes 10 seconds to execute.
{
// sleep for 10 seconds.
Thread.Sleep(10000);
}
Normally, when your application calls the function Foo()
, it will need to wait 10 seconds until Foo()
is finished and control is returned to the calling thread. Now, suppose you want to call Foo()
100 times, then we know that it would take 1000 seconds for the control to return to the calling thread. This type of a method invocation is Synchronous.
- Call
Foo()
Foo()
is executed- Control goes back to the calling thread
Let's now call Foo()
using delegates because most of the work we will do here is based on delegates. Luckily for us, there is already a delegate within the .NET framework that allows us to call a function that takes no parameter and has no return value. The delegate is called MethodeInvoker
. Let's play with it a little.
// to our Foo() function.
MethodInvoker simpleDelegate = new MethodInvoker(Foo);
// Calling Foo
simpleDelegate.Invoke();
Even with the example above, we are still calling Foo()
synchronously. The calling thread still needs to wait for the Invoke()
function to complete until the control is returned to the calling thread.
Asynchronous method invocation
But what if I wanted to call Foo()
and not wait for it to finish executing? In fact, to make things interesting, what if I didn�t care when it is finished? Let�s say, I just wanted to call Foo
100 times without waiting for any of the function calls to complete. Basically, doing something called Fire and Forget. You call the function, you don�t wait for it, and you just forget about it. And� let�s not forget! I am not willing to change a line of code in my super complicated fancy Foo()
function.
Let me make a few comments about the code above.
- Notice that
BeginInvoke()
is the line of code that executes theFoo()
function. However, the control is returned to the caller right away, without waiting forFoo()
to complete. - The code above does not know when a call to
Foo()
completes, I will cover that later. BeginInvoke()
is used instead ofInvoke()
. For now, don�t worry about the parameters this function takes; I will cover that in more detail later.
What is the magic that .NET is doing in the background
Once you ask the framework to call something asynchronously, it needs a thread to do the work. It can not be the current thread, because that would make the invocation synchronous (blocking). Instead, the runtime queues a request to execute the function on a thread from the .NET Thread Pool. You don�t really need to code anything for it, all of it happens in the background. But, just because it is all transparent doesn�t mean you should care about it. There are a few things to remember:
Foo()
is executed on a separate thread, a thread that belongs to the .NET Thread Pool.- A .NET Thread Pool normally has 25 threads in it (you can change that limit), and each time
Foo()
is called, it is going to be executed on one of these threads. You can't control which one. - The Thread Pool has its limits! Once all the threads are used, an async method invocation is queued until one of the threads from the pool is freed. This is called Thread Pool Starvation, and normally when it comes to that, performance is compromised.
Don�t dive too deep into the thread pool, you might run out of oxygen!
So, let's see an example of when the Thread Pool is starved. Let's modify our Foo
function to wait for 30 seconds, and also let it report the following:
- Number of avaible threads on the pool
- If the thread is on the thread pool
- The thread ID.
We know that initially the thread pool contains 25 threads, so I am going to call my Foo
function asynchronously 30 times (to see what happens after the 25th call).
Output window:
Let�s make a few notes about the output:
- Notice, first of all, that all the threads are on the thread pool.
- Notice that each time
Foo
is called, another thread ID is assigned. However, you can see that some of the threads are recycled. - After calling
Foo()
25 times, you can see that there are no more free threads on the pool. At this point, the application �waits� for a free thread. - Once a thread is freed, the program grabs it right away, calling
Foo()
, and still there are 0 free threads on the pool. This continues to happen untilFoo()
is called 30 times.
So right away, not doing anything too fancy, we can make a few comments about calling methods asynchronously.
- Know that your code will run in a separate thread, so some thread safety issues may apply. This is a topic on its own, and I will not cover it here.
- Remember that the pool has its limits. If you plan to call many functions asynchronously and if they take a long time to execute, Thread Pool Starvation might occur.
BeginInvoke() and EndInvoke()
So far we saw how to invoke a method without really knowing when it is finished. But with EndInvoke()
, it is possible to do a few more things. First of all, EndInvoke
will block until your function completes execution; so, calling BeginInvoke
followed by EndInvoke
is really almost like calling the function in a blocking mode (because the EndInvoke
will wait until the function completes). But, how does the .NET runtime know how to bind a BeginInvoke
with an EndInvoke
? Well, that�s where IAsyncResult
comes in. When calling BegineInvoke
, the return object is an object of type IAsyncResult
; it is the glue that allows the framework to track your function execution. Think of it like a little tag to let you know what is going on with your function. With this little powerful super tag, you can find out when your function completes execution, and you can also use this tag to attach any state object you might want to pass to your function. Okay! Let�s see some examples so this doesn't become too confusing... Let's create a new Foo
function.
{
// sleep for one second!
Thread.Sleep(1000);
}
What about Exceptions, how do I catch them?
Now, let's make it a little more complicated. Let me modify the FooOneSecond
function and make it throw an exception. Now, you should be wondering how you will catch this exception. In the BeginInvoke
, or in the EndInvoke
? Or is it even possible to catch this exception? Well, it is not in the BeginInvoke
. The job of BeginInvoke
is to simply start the function on the ThreadPool
. It is really the job of the EndInvoke
to report all the information about the completion of the function, and this includes exceptions. Notice the next snippet of code:
{
// sleep for one second!
Thread.Sleep(1000);
// throw an exception
throw new Exception("Exception from FooOneSecond");
}
Now, let's call FooOneSecond
and see if we can catch the exception.
By running the code, you will see that the exception is only thrown and caught when calling EndInvoke
. If you decide to never call EndInvoke
, then you will not get the exception. However, when running this code within the debugger, depending on your exception settings, your debugger might stop when throwing the exception. But that is the debugger. Using a release version, if you don�t call EndInvoke
, you will never get the exception.
Passing parameters to your method
Okay, so calling functions without parameters is not going to take us very far, so I am going to modify my super fancy and sophisticated Foo
function to take a few parameters.
Let's call FooWithParameters
using BeginInvoke
and EndInvoke
. First of all, before we do anything, we must have a delegate that matches the signature of this method.
int param2, ArrayList list);
Think of BeginInvoke
and EndInvoke
as cutting our function into two separate methods. The BeginInvoke
is responsible for accepting all the input parameters followed by two additional parameters every BeginInvoke
has (callback delegate, and a state object). The EndInvoke
is responsible for returning all output parameters (parameters marked with ref
or out
) and a return value, if there is one. Let's go back into our example to find out what are considered input parameters and what are output parameters. param1
, param2
, and list
are all considered input parameters, and therefore, they will be accepted as arguments to the BeginInvoke
method. The return value of type string
is considered an output parameter, and therefore, it will be the return type for EndInvoke
. The cool thing is that the compiler is able to generate the correct signature for BeginInvoke
and EndInvoke
based on the declaration of your delegate. Notice that I decided to modify the values of my input parameters just to examine if the behaviour is as I expect it to be without calling BeginInvoke
and EndInvoke
. I also re-allocate the ArrayList
that is passed to a new ArrayList
. So, try to guess what the output is going to be...
Let's see our FooWithParameters
again, just so you don't need to scroll up.
Let me give you the three lines from the output window after calling EndInvoke()
:
param2: 100
ArrayList count: 1
Okay, let�s analyze all this. Even when my function modifies the values of the input parameters, we don�t get to see those changes after calling EndInvoke
. The string is a mutable type, therefore, a copy of the string is created, and the change is not passed back to the caller. Integers are value types, and they create a copy when passed by value. Finally, re-creating the ArrayList
is not returned to the caller because the reference to the ArrayList
is passed by value, and in fact, re-creating the ArrayList
is simply creating a new allocation for ArrayList
assigning the "copied" reference that was passed. In fact, that reference is lost, and normally considered as a memory leak; but luckily for us, the .NET garbage collector will eventually grab it. So, what if we wanted to get back our new allocated ArrayList
and the rest of the changes we did to our parameters? What do we need to do? Well, it is simple; we simply have to tag the ArrayList
as a ref
parameter. Just for fun, let�s also add output parameters just to show how EndInvoke
is changed.
Let us see what is considered an output parameter and what is considered an input parameter�
Param1
is an input parameter, it will only be accepted withinBeginInvoke
.Param2
is input and output; therefore, it will be passed to bothBeginInvoke
andEndInvoke
(EndInvoke
will give us the updated value).list
is passed by reference, and therefore, it is too going to be passed to bothBeginInvoke
andEndInvoke
.
Let�s see how our delegate looks like now:
out int param2, ref ArrayList list);
and finally, let's look at the function that calls FooWithOutAndRefParameters
:
Here is the output:
param2: 200
ArrayList count: 0
return value: Thank you for reading this article
Notice that param1
does not change. It is an input parameter, and param2
was passed as an output parameter and was updated to 200. The array list has been reallocated, and now we see that it is pointing to a new reference of zero elements (the original reference is lost). I hope that now you understand how parameters are passed with BeginInvoke
and EndInvoke
. Let�s move on to looking at how to be notified if a non-blocking function is completed.
What they don�t want you to know about IAsyncResult
You should be wondering how EndInvoke
is able to give us the output parameters and the updated ref
parameters. Or, better yet, how EndInvoke
is able to throw that exception we threw in our function. For example, say we called BegineInvoke
on Foo
, and then Foo finished executing, and now, we normally would call EndInvoke
, but what if we decide to call EndInvoke
20 minutes after Foo
is finished? Notice that EndInvoke
will still give you those output or ref
parameters, and it would still throw that exception (if one was thrown). So, where is all that information stored? How come EndInvoke
is able to get all that data long after the function is completed? Well� the key is with the IAsyncResult
object! I decided to explore this object a little more, and as I suspected, it is this object that keeps all the information regarding your function call. Notice that EndInvoke
takes one parameter, it is an object of type IAsyncResult
. This object contains information such as:
- Is the function completed?
- A reference to the delegate used for
BeginInvoke
- All output parameters and their values
- All
ref
parameters and their updated values - Return value
- An Exception if one was thrown
- And more�
IAsyncResult
may seem very innocent, because it is just an interface to a few little properties, but in fact, it is an object of type System.Runtime.Remoting.Messaging.AsyncResult
.
Now, if we dig a little deeper, we will find that AsyncResult
contains an object called _replyMsg
of type System.Runtime.Remoting.Messaging.ReturnMessage
, and what do you know� the holy grail has been found!
I had to shrink the above image so you won�t need to scroll to the right to read it, you can simply click on the image to view it
We can clearly see our return value, our output parameter, and ourref
parameters. There is even an exception property to hold the exception. Notice that I expanded, in the debug window for OutArgs
to show, the value 200 and a reference to the newly allocated ArrayList
. You can also see in the property ReturnValue
, the string �Thank you for reading this article�. If we had an exception, then the EndInvoke
would throw it for us to catch it. I think this is proof enough to conclude that all the information regarding your function call is saved with that little IAsyncResult
object you get back from BeginInvoke
, it is like a key to your data. If we lose this object, we will never know our output parameters, ref
parameters, and return value. It will also not be possible to catch an exception without this object. It�s the key! You lose it, and the info is lost forever in the maze of the .NET runtime� OK, getting a little carried away here. I think I made my point. Using the Callback delegate, Hollywood style "Don�t call me I will call you!"
At this point, you should understand how parameters can be passed, how to pass state, and understand the fact that your method is executed on a thread within the ThreadPool
. The only thing I didn�t really cover is the idea of being notified when the method is finished executing. After all, blocking and waiting for the method to finish does not accomplish much. In order to be notified when a method is complete, you must supply a callback delegate on the BeginInvoke
. OK, example! Look at the following two functions:
In here, you can see that we passed a delegate to the function CallBack
when calling BeginInvoke
. .NET will call us when the method FooWithOutAndRefParameters
completes execution. As before, we all know that we must call EndInvoke
if we want to get our output parameters. Notice that in order to call EndInvoke
, I needed to do some gymnastics to get the delegate.
// grab the delegate
DelegateWithOutAndRefParameters del =
(DelegateWithOutAndRefParameters) result.AsyncDelegate;
Wait a minute! On which thread is the call-back executed on?
After all, the callback is invoked by .NET using your delegate, but still it is .NET that calls this delegate. It is your right and duty to know on which thread your code is executed on. To give a clear picture of what is going on, I decided to yet again modify my Foo
function to include thread information and add a delay of 4 seconds.
I also added thread information to the callback function:
I decided to execute FooWithOutAndRefParameters
multiple times, using a button on my form.
{
CallFooWithOutAndRefParametersWithCallback();
}
Let�s see the output after pressing my button thrice (calling the function thrice):
Notice that my Foo
function is executed thrice, one after the other, on three separate threads. All the threads are on the thread pool. Notice also that the callback is also executed thrice, respectively, and they are all on the thread pool too. What makes this interesting is that, the callback seems to be executed on the same thread ID as Foo
. Thread 7 executes Foo
; 4 seconds later, the callback is also executed on thread 7. The same with thread 12 and 13. It is like the callback is a continuation of my Foo
function. I pressed my button many times, trying to see if the callback will ever be called on a thread Id other then the one Foo
is executed on, I was not able to achieve that. If you think about it, it makes total sense. Imagine, .NET would grab a thread to call Foo
and then grab another thread to call the callback, that would be a waste! Not to mention that if your thread pool is starved, you will end up waiting for a free thread just to call the callback! That would have been a disaster.
Using the Command Pattern to clean things up!
Okay! Let's face it, things are getting a little messy. We have a BeginInvoke
, EndInvoke
, a callback, and they are all over the place! Let's try to use the Command Pattern to clean up the method invocation. Using the Command Pattern is simple and easy. Basically, you create a Command object that implements a simple interface like:
{
void Execute();
}
It it time that we stop using our useless Foo
function everywhere, and try to do something more real! So, let's create a scenario that is more realistic. Say, we have the following:
- We have a user Form that contains a grid that displays customer rows.
- The Grid is updated with rows based on a search criteria of customer ID. However, the database is far, far away, and it takes 5 seconds to get the customer dataset; we do not wish to block the UI while we are waiting.
- We have a nice business object that is responsible to get our customer dataset based on a customer ID.
Suppose this was our Business Layer. To keep the example simple, I hard-coded what would normally come from a data layer.
Now, let's create our Command, which is responsible to update the grid based on the customer ID.
Notice that the GetCustomerByIdCommand
takes all the information it needs to execute the command.
- The grid to update.
- The customer ID to search.
- A reference to the business layer.
Also notice that the delegate is hidden within the Command object, so the client doesn�t need to know the inner working of the Command. All the client needs to do is build the Command and call Execute
on it. We all know by now that asynchronous methods invocation is done on the ThreadPool
, and we should all know by now that it is not healthy to update the UI from the ThreadPool
or any other thread other then the UI thread! So, to fix this problem, we hide this implementation within the Command, and check based on the Grid if InvokeRequired()
is true. If it is true, we use Control.Invoke
to make sure the call is marshaled to the UI thread. (Notice, I am using the .NET 2.0 features of creating anonymous methods.) Let's see how the form is creating the command and executing it!
Notice, that Execute
is non-blocking. But before you go nuts and create a million command classes, keep these in mind:
- The Command Pattern may cause class explosion, so choose your weapon wisely.
- In my case, it would have been easy to create a base class to my command that has the logic to update a grid in a thread-safe manner, but I kept my example simple.
- It would also be acceptable to pass the
TextBox
into the command object so it can grab the input in a more dynamic way and allow the command to be called anytime without re-creating it. - Notice that the delegate,
BeginInvoke
,EndInvoke
, callback, and our crazy code to make sure the UI is updated in a thread safe manner is all encapsulated in my Command, which is a good thing!
Conclusion
Phew! It took me almost a week to write this fun article. I tried to cover all the important aspects of calling a method in a non-blocking mode. Here are a few things to remember:
- Delegates will contain the correct signature for
BeginInvoke
andEndInvoke
, you should expect all output parameters and exceptions to come out when callingEndInvoke
. - Don�t forget you take juice from the
ThreadPool
when usingBeginInvoke
, so don�t overdue it! - If you plan to use a callback, it might be a good idea to use the Command Pattern to hide all the nasty code that goes with it.
- Personally, the UI should only be blocked when doing UI stuff, so now you have no excuse!