您的位置:首页 > 产品设计 > UI/UE

[转]WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

2011-08-10 18:21 423 查看
This blog comes from here:

http://weblogs.asp.net/justin_rogers/pages/126345.aspx

Abstract:
Marshalling the execution of your code
onto the UI thread in the Windows Forms environment is critical to
prevent cross-thread usage of UI code. Most people don't understand how
or when they'll need to use the marshalling behavior or under what
circumstances it is required and when it is not. Other users don't
understand what happens when you use the marshalling behavior but it
isn't needed. In actuality it has no negative effects on stability, and
instead reserves any negative side effects to performance only.

Understanding the semantics of when your callback methods will be
called, in what order, and how might be very important to your
application. In addition to the default marhalling behavior, I'll be
covering special considerations for enhancing the marhsalling behavior
once we fully understand how it works. We'll also cover all of the
normal scenarios and uses for code execution marhsalling to make this a
complete Windows Forms marshalling document.

TOC:

UCS 1: Using InvokeRequired and Invoke for Synchronous Marshalling, the default scenario

UCS 2: Using BeginInvoke for Asynchronous Marshalling

InvokeRequired and how it works

Invoke operation on the UI thread and from a different thread

InvokeMarshaledCallbacks and how it handles the callback queue

BeginInvoke operation on the UI thread and from a different thread

UCS 3: Using BeginInvoke to change a property after other events are processed, and why it can fail

Public and Internal Methods covered with a short description of what they do

Conclusion

1. UCS 1: Using InvokeRequired and Invoke for Synchronous Marshalling, the default scenario
I
call this the default scenario, because it identifies the most
prominent use of UI thread marshalling. In this scenario the user is
either on the UI thread or they are not, and most likely they aren't
sure. This can occur when you use common helper methods for acting on
the UI that are called from your main code (most likely on the UI
thread), and in code running on worker threads.

You can always tell if an Invoke is going to be required by calling
InvokeRequired. This method finds the thread the control's handle was
created on and compares it to the current thread. In doing so it can
tell you whether or not you'll need to marshal. This is extremely easy
to use since it is a basic property on Control. Just be aware that
there is some work going on inside the method and it should have
possibly been made a method instead.



Button b = new Button(); // Creates button on the current thread
if ( b.InvokeRequired ) { // This shouldn't happen since we are on the same thread }
else { // We should fall into here }



If your code is running on a
thread that the control was not created on then InvokeRequired will
return true. In this case you should either call Invoke or BeginInvoke
on the control before you execute any code. Invoke can either be called
with just a delegate, or you can specify arguments in the form of an
object[]. This part can be confusing for a lot of users, because they
don't know what they should pass to the Invoke method in order to get
their code to run. For instance, let's say you are trying to do
something simple, like call a method like Focus(). Well, you could
write a method that calls Focus() and then pass that to Invoke.



myControl.Invoke(new MethodInvoker(myControl.Hide());



Noticed I used MethodInvoker.
This is a special delegate that takes no parameters so it can be used to
call any methods that take 0 parameters. In this case Focus() takes no
arguments so things work. I'm telling the control to invoke the method
right off of myControl, so I don't need any additional information.
What happens if you need to call a bunch of methods on myControl? In
that case you'll need to define a method that contains all of the code
you need run and then Invoke it.



private void BunchOfCode() {
myControl.Focus();
myControl.SomethingElse();
}

myControl.Invoke(new MethodInvoker(this.BunchOfCode());



This solves one problem, but
leaves another. We just wrote code that only works only for myControl
because we hard coded the control instance into our method. We can
overcome this by using an EventHandler syntax instead. We'll cover the
semantics of this later, so I'll just write some code that works now.



private void BunchOfCode(object sender, EventArgs e) {
Control c = sender as Control;
if ( c != null ) {
c.Focus();
c.SomethingElse();
}
}

myControl.Invoke(new EventHandler(BunchOfCode));



EventArgs is always going to be
empty, while sender will always be the control that Invoke was called
on. There is also a generic helper method syntax you can use to
circumvent any of these issues that makes use of InvokeRequired. I'll
give you a version of that works with MethodInvoker and one that works
with EventHandler for completeness.



private void DoFocusAndStuff() {
if ( myControl.InvokeRequired ) {
myControl.Invoke(new MethodInvoker(this.DoFocusAndStuff));
} else {
myControl.Focus();
myControl.SomethingElse();
}
}

private void DoFocusAndStuffGeneric(object sender, EventArgs e) {
Control c = sender as Control;

if ( c != null ) {
if ( c.InvokeRequired ) {
c.Invoke(new EventHandler(this.DoFocusAndStuffGeneric));
} else {
c.Focus();
c.SomethingElse();
}
}
}



Once you've set up these helper
functions, you can just call them and they handle cross thread
marshalling for you if needed. Notice how each method simply calls back
into itself as the target of the Invoke call. This lets you put all of
the code in a single place. This is a great abstraction that you can
add to your application to automatically handle marshalling for you. We
haven't yet had to define any new delegates to handle strange method
signatures, so these techniques have low impact on the complexity of
your code. I'll wrap up the Invoke use case scenario there and move
into the BeginInvoke scenario.

2. UCS 2: Using BeginInvoke for Asynchronous Marshalling
Whenever
you call Invoke, you have to wait for the return call, so your current
thread hangs until the remote operation completes. This can take some
time since lots of things need to happen in order to schedule your code
on the UI thread and have it execute. While you don't really have to
worry that an Invoke might block indefinitely, you still can't determine
exactly how long it will take (unless it really wasn't required in the
first place, but we'll get to that later). In these cases you'll want
to call Invoke asynchronously.

Calling your code asynchronously
is simliar to calling it through Invoke. The only difference is that
BeginInvoke will return immediately. You can always check for the
results of your operation by calling EndInvoke, but you don't have to.
In general, you'll almost never use EndInvoke unless you actually want
the return value from the method which is fairly rare. The same
plumbing is in the back-end for BeginInvoke as for Invoke so all we'll
be doing is changing our code from UCS 1 to use BeginInvoke.



private void DoFocusAndStuff() {
if ( myControl.InvokeRequired ) {
myControl.BeginInvoke(new MethodInvoker(this.DoFocusAndStuff));
} else {
myControl.Focus();
myControl.SomethingElse();
}
}

private void DoFocusAndStuffGeneric(object sender, EventArgs e) {
Control c = sender as Control;

if ( c != null ) {
if ( c.InvokeRequired ) {
c.BeginInvoke(new EventHandler(this.DoFocusAndStuffGeneric));
} else {
c.Focus();
c.SomethingElse();
}
}
}



What happens if you do need the
return value? Well, then the use case changes quite a bit. You'll need
to wait until the IAsyncResult has been signalled complete and then
call EndInvoke on this object to get your value. The following code
will will grab the return value and then immediately call EndInvoke.
Note that since the result is probably not ready yet, EndInvoke will
hang. Using this combination of BeginInvoke/EndInvoke is the same as
just calling Invoke.



IAsyncResult result = myControl.BeginInvoke(new MethodInvoker(myControl.Hide());
myControl.EndInvoke(result);



So we'll change our behavior to
check for completion status. We'll need to find some way to poll the
completion status value so we don't hang our current thread and can
continue doing work while we wait. Normally you'll just put places in
your code to check the result status and return. We don't have the time
nor space to make up such an elaborate sample here, so we'll just
pretend we are doing work.



IAsyncResult result = myControl.BeginInvoke(new MethodInvoker(myControl.Hide());
while ( !result.IsCompleted ) { // Do work somehow }
myControl.EndInvoke(result);



The BeginInvoke use case
scenario isn't much different from the Invoke scenario. The underlying
reason behind using one over the other is simply how long you are
willing to wait for the result. There is also the matter of whether you
want the code to execute now or later. You see, if you are on the UI
thread already and issue an Invoke the code runs immediately. If you
instead issue a BeginInvoke you can continue executing your own code,
and then only during the next set of activity on the message pump will
the code be run. If you have some work to finish up before you yield
execution then BeginInvoke is the answer for you.

You have to be careful when
using BeginInvoke because you never know when your code will execute.
The only thing you are assured is that your code will be placed on the
queue and executed in the order it was placed there. This is the same
guarantee you get for Invoke as well, though Invoke places your code on
the queue and then exhausts it (running any queued operations). We'll
examine this in more detail in later sections. For now, let's take a
hard look at InvokeRequired.

3. InvokeRequired and how it works
This
is a read-only property that does quite a bit of work. You could say
it ran in determinate time in most cases, but there are degenerate cases
where it can take much longer. In fact the only time it is determinate
is if IsHandleCreated is true meaning the control you are using is
fully instantiated and has a windows handle associated with it.

If the handle is created then
control falls into the check logic to see if the windows thread process
id is the same as the current thread id. They use
GetWindowThreadProcessID, a Win32 API call, to check the handle and find
it's thread and process ID (note the process ID doesn't appear to be
used). Then they grab the current thread ID through none other than
GetCurrentThreadID. The result of InvokeRequired is nothing more than
(threadID != currentThreadID). Pretty basic eh?

Things get more difficult when
your control's handle is not created yet. In this case they have to
find what they call a marshalling control for your control. This
process can take some time. They walk the entire control hiearchy
trying to find out if any of your parent control's have been
instantiated yet and have a valid handle. Normally they'll find one.
As soon as they do they fall out and return that control as your
marshalling control. If they can't find any the have a fallback step.
They get the parking window. They make one of these parking windows on
every thread that has a message pump apparently, so no matter where you
create your controls (no matter what thread) there should be at least
one control that can be used as the marshalling control (unless maybe
you are running in the designer ;-).

Application.GetParkingWindow is
nasty. After all, this is the final fallback and the last ditch effort
to find some control that can accept your windows message. The funny
thing here is that GetParkingWindow is extremely determinant if your
control is already created. They have some code that basically gets the
ThreadContext given the thread ID of your control. That is what we've
been looking for this entire time, so that code-path must be used
somewhere else (darn IL is getting muddied, thank god these are small
methods).

Then they start doing the
magic. They assume the control is on the current thread. This is just
an assumption, and it might not be true, but they make it for the sake
of running the method. They get the parking window off of this current
TheadContext and return that. If it hasn't been created yet, we are
really screwed because that was our last chance to find a marshalling
control. At this point, if we still don't have a marshalling control,
they return the original control you passed in.

At the end of this entire
process, if we find a marshalling control, that is used with
GetWindowThreadProcessID. If not, we simply return false, indicating
that an Invoke is not required. This is important. It basically means
if the handle isn't created, it doesn't matter WHAT thread you are on
when you call into the control. Reason being, is that there isn't any
Handle, which means no real control exists yet, and all of the method
calls will probably fail anyway (some won't, but those that require a
HWND or Windows Handle will). This also means you don't always have to
call control methods on the UI thread, only those that aren't thread
safe. With InvokeRequired to the side, it is time to talk about Invoke
and what it goes through.

4. Invoke operation on the UI thread and from a different thread
Time
to examine the Invoke operation and what is involed. To start with,
we'll examine what happens when the Invoke operation is happening on the
same thread as the UI thread for the control. This is a special case,
since it means we don't have to marshal across a thread boundary in
order to call the delegate in question.

All of the real work happens in
MarshaledInvoke. This call is made on the marshalling control, so the
first step is to get the marshaling control through
FindMarshalingControl. The first Invoke method, without arguments,
calls the Invoke method with a null argument set. The overriden Invoke
in turn calls MarshaledInvoke on the marshaling control passing in the
current caller (note we need this because the marshalling control might
be different from the control we called Invoke on), the delegate we are
marshalling, the arguments, and whether or not we want synchronous
marshaling. That second parameter is there so we can use the same
method for asynchronous invokes later.



// The method looks something like this and it is where all of the action occurs
object MarshaledInvoke(Control invokeControl, Delegate delegate, object[] arguments, bool isSynchronous);



If the handle on the marhaling
control is invalid, you get the classic exception telling you the handle
isn't created and that the Invoke or what not failed. There is also
some gook about ActiveX controls in there that I don't quite understand,
but they appear to be demanding some permissions. Then comes the
important part for calling Invoke on the UI thread. They again check
the handle's thread id against the current thread id, and if we are
running synchronously, they set a special bool indicating we are running
synchronously and are operating on the same thread. This is the
short-circuit code that gets run only when you call Invoke and are on
the same thread.

Since the special case is
enabled, we'll immediately call the InvokeMarshaledCallbacks method
rather than posting a message to the queue. Note all other entries into
this method, and all other conditions will cause a windows message to
be posted and InvokeMarshaledCallbacks will later be called from the
WndProc of the control once the message is received.

There is some more code before
this point. Basically, they make a copy of the arguments you pass in.
This is pretty smart, since I'm guessing you could try changing the
arguments in the original array and thus the arguments to your delegate
if they didn't make the copy. It also means, once Invoke or BeginInvoke
is called, you can change your object array of parameters, aka you can
reuse the array, which is pretty nice for some scenarios.

After they copy your parameters
into a newly allocated array they take the liberty of grabbing the
current stack so they can reattach it to the UI thread. This is for
security purposes so you can't try to Invoke code on the UI thread that
you wouldn't have been able to run on your own thread. They use
CompressedStack for this operation and the GetCompressedStack method.
While this is a public class inside of mscorlib.dll, there is NO
documentation for it. It seems to me that this might be a very
interesting security mechanism for API developers, but they don't give
you any info on it. Maybe I'll write something about how to use it
later.

With this in place, they
construct a new ThreadMethodEntry. These guys are the work horse. They
get queued into a collection, and are later used to execute your
delegate. It appears the only additional parameter used to create this
class over calling MarshaledInvoke is the CompressedStack. They also
used the copied arguments array instead of the original.

They then grab the queue for
these guys off of the property bag. You could never do this yourself,
because they index the properties collection using object instances that
you can't get access to. This is a very interesting concept, to create
an object used to index a hashtable or other collection that nobody
else has access to. They store all of the WinForms properties this way,
as well as the events.

Finally, they queue the
ThreadMethodEntry onto the queue and continue. They appear to do a
bunch of locking to make all of this thread-safe. While the Invoke
structure is a pain in the rear, I'm glad they reserve all of this
locking to a few select methods that handle all of the thread safe
operations.

Since this is an Invoke there is
additional code required to make sure the operation happens
synchronously. The ThreadMethodEntry implements IAsyncResult directly,
so on Invoke calls, we check to make sure it isn't already completed (a
call to IsCompleted), and if it isn't, we grab the AsyncWaitHandle and
do a WaitOne call. This will block our thread until the operation
completes and we can return our value. Why did we make a call to
IsCompleted first? Well, remember that call we made to
InvokeMarshaledCallbacks? Well, when we do that our operation will
already be complete once we get to that portion of the code. If we
didn't make this check and instead just started a WaitOne on the handle,
we'd hang indefinitely.

Once the operation either
completes or was already completed, we look for any exceptions. If
there are exceptions, we throw them. Here have some exceptions they say
;-) If no exceptions were thrown then we return a special return value
property stored on the ThreadMethodEntry. This value is set in
InvokeMarshaledCallbacks when we invoke the delegate.

If you are running off the UI
thread, how do things change? Well, we don't have the special same
thread operation involved this time, so instead we post a message to the
marshaling control. This is a special message that is constructed
using some internal properties and then registered using
RegisterWindowMessage. This ensures that all controls will use the same
message for this callback preventing us from register a bunch of custom
windows messages.

InvokeMarshaledCallbacks is an
important method since it gets called both synchronously if we are on
the same thread as the UI and from the WndProc in the case we aren't.
This is where all of the action of calling our delegate happens and so
it is where we'll be next.

5. InvokeMarshaledCallbacks and how it handles the callback queue
This
method is deep. Since it has to be thread safe, we get lots of locking
(even though we should only call this method from the UI thread, we
have to make sure we don't step on others that are accessing the queue
to add items, while we remove them). Note that this method will
continue processing the entire queue of delegates, and not just one.
Calling this method is very expensive, especially if you have a large
number of delegates queued up. You can start to better understand the
performance possibilities of asynchronous programming and how you should
avoid queuing up multiple delegates that are going to do the same thing
(hum, maybe that IAsyncResult will come in handy after all ;-)

We start by grabbing the
delegate queue and grabbing a start entry. Then we start up a loop to
process all of the entries. Each time through the loop the current
delegate entry gets updated and as soon as we run out of elements, the
loop exits. If you were to start an asynchronous delegate from inside
of another asynchronous delegate, you could probably hang your system
because of the way this queue works. So you should be careful.

The top of the loop does work
with the stack. We grab the current stack so we can restore it later,
then set the compressed stack that was saved onto the
ThreadMethodEntry. That'll ensure our security model is in place. Then
we run the delegate. There are some defaults. For instance, if the
type is MethodInvoker, we cast it and call it using a method that yields
better performance. If the method is of type EventHandler, then we
automatically set the parameters used to call the EventHandler. In this
case the sender will be the original caller, and the EventArgs will be
EventArgs.Empty. This is pretty sweet, since it simplifies calling
EventHandler definitions. It also means we can't change the sender or
target of an EventHandler definition, so you have to be careful.

If the delegate isn't of one of
the two special types then we do a DynamicInvoke on it. This is a
special method on all delegates and we simply pass in our argument
array. The return value is stored on our ThreadMethodEntry and we
continue. The only special case is that of an exception. If an
exception is thrown, we store the exception on the ThreadMethodEntry and
continue.

Exiting our delegate calling
code, we reset the stack frame to the saved stack frame. We then call
Complete on our ThreadMethodEntry to signal anybody waiting for it to
finish. If we are running asynchronously and there were exceptions we
call Application.OnThreadException(). You may have noticed these
exceptions happening in the background when you call BeginInvoke in your
application, and this is where they come from. With all of that
complete, we are done. That concludes all of the code required to
understand an Invoke call, but we still have some other cases for
BeginInvoke, so let's look at those.

6. BeginInvoke operation on the UI thread and from a different thread
How
much different is BeginInvoke from the basic Invoke paradigm? Well,
not much. There are only a couple of notes, so I don't take a bunch of
your time redefining all of the logic we already discussed. The first
change is how we call MarshaledInvoke. Instead of specifying true for
running synchronously we instead specify false. There is also no
special case for running synchronously on the UI thread, instead we
always post a message to the windows pump. Finally, rather than having
synchronization code on the ThreadMethodEntry, we return it immediately
as an IAsyncResult that can be used to determine when the method has
completed later or with EndInvoke.

That is where all of the new
logic is, EndInvoke. You see, we need additional logic for retrieving
the result of the operation and making sure it is completed. EndInvoke
can be a blocking operation if IsCompleted is not already true on the
IAsyncResult. So basically, we do a bunch of checks to make sure the
IAsyncResult passed in really is a ThreadMethodEntry. If it is, and it
hasn't completed, we do the same synchronization logic we did on the
Invoke version, with some small changes. First, we try to do an
InvokeMarshaledCallbacks if we are on the same thread. This is similar
to the same thread synchronization we did in the first case. If we
aren't on the same thread, then we wait on the AsyncWaitHandle. They
have some code that is dangerously close to looking like a race
condition here, but I think they've properly instrumented everything to
prevent that scenario.

As we fall through all of the
synchronization we again check for exceptions. Just like with Invoke we
throw them if we have them. A lot of people don't catch these
exceptions or assume they won't happen, so a lot of asynchronous code
tends to fail. Catch your exceptions people ;-) If no exceptions were
thrown then we return the value from the delegate and everything is
done.

You see, not many changes are
required in order to implement BeginInvoke over top of the same code we
used in Invoke. We've already covered the changes in
InvokeMarshaledCallbacks, so we appear to be complete. Time for a
sample.

7. UCS 3: Using BeginInvoke to change a property after other events are processed, and why it can fail
Sometimes
events in Windows Forms can transpire against you. The classic example
I use to explain this process is the AfterNodeSelect event of the
TreeView control. I generally use this event in order to update a
ListBox or other control somewhere on the form, and often you want to
transfer focus to a new control, probably the ListBox. If you try to
set the Focus within the event handler, then later on when the TreeView
gets control back after the event, it sets the Focus right back to
itself. You feel like nothing happened, even though it did.

You can easily fix this by using
a BeginInvoke to set focus instead. We'll call Focus directly so we
need to define a new delegate. We'll call it a BoolMethodInvoker since
Focus() returns a bool, we can't just use the basic MethodInvoker
delegate (what a shame eh?)



// Declare the delegate outside of your class or as a nested class member
private delegate bool BoolMethodInvoker();

// Issue this call from your event instead of invoking it directly.
listPictures.BeginInvoke(new BoolMethodInvoker(listPictures.Focus));



Now, knowing a bit about how the BeginInvoke stuff works,
there is a way to screw yourself over. First, your method may get
executed VERY soon. As a matter of fact, the next message on the pump
might be a marshalling message, and then other messages in the pump that
you wanted to go after might still be executed after you. In many
cases your method calls will still generate even more messages so this
can be circumvented a bit, but possibly not.

There is a second issue as well. If another code source
calls an Invoke and you are on the UI thread, then your method may get
processed even before the event handlers are done executing and the
TreeView gets control back to make it's focus call. This is an edge
case, but you can imagine you might run into scenarios where you want
some asynchronous operations and some synchronous. You need to be aware
than any synchronous call can possibly affect your asynchronous calls
and cause them to be processed.

8. Public and Internal Methods covered with a short description of what they do
These
are all of the public and internal methods that we covered and what
they do. Kind of a quick reference. I'll probably find this very
helpful later when I'm trying to derive some new functionality and I
don't want to have to read my entire article.

InvokeRequired - Finds the most
appropriate control and uses the handle of that control to get the
thread id that created it. If this thread id is different than the
thread id of the current thread then an invoke is required, else it is
not. This method uses a number of internal methods to solve the issue
of the most appropriate control.

Invoke - This method sets up a brand new
synchronous marshalled delegate. The delegate is marshalled to the UI
thread while your thread waits for the return value.

BeginInvoke - This method sets up a
brand new asynchronous marshalled delegate. The delegate is marshalled
to the UI thread while your thread continues to operate. An extended
usage of this method allows you to continue working on the UI thread and
then yield execution to the message pump allowing the delegate to be
called.

EndInvoke - This method allows you to
retrieve the return value of a delegate run by the BeginInvoke call. If
the delegate hasn't returned yet, EndInvoke will hang until it does.
If the delegate is alread complete, then the return value is retrieved
immediately.

MarshaledInvoke - This method queues up
marshaling actions for both the Invoke and BeginInvoke layers.
Depending on the circumstances this method can either immediately
execute the delegates (running on the same thread) or send a message
into the message pump. It also handles wait actions during the Invoke
process or returns an IAsyncResult for use in BeginInvoke.

InvokeMarshaledCallbacks - This method
is where all of your delegates get run. This method is either called
from MarshaledInvoke or WndProc depending on the circumstances. Once
inside of this method, the entire queue of delegates is run through and
all events are signalled allowing any blocking calls to operate (Invoke
or EndInvoke calls) and setting all IAsyncResult objects to the
IsCompleted = true state. This method also handles exception logic
allowing exceptions to be thrown back on the original thread for Invoke
calls or tossed into the applications thread exception layer if you are
using BeginInvoke and were running asynchronous delegates.

FindMarshallingControl - Walks the
control tree from current back up the control hierarchy until a valid
control is found for purposes of finding the UI thread id. If the
control hierarchy doesn't contain a control with a valid handle, then a
special parking window is retrieved. This method is used by many of the
other methods since a marshalling control is the first step in
marshalling a delegate to the UI thread.

Application.GetParkingWindow - This
method takes a control and finds the marking window for it. If the
control has a valid handle then the thread id of the control is found,
the ThreadContext for that thread is retreived, and the parking window
is returned. If the control does not have a valid handle then the
ThreadContext of the current thread is retrieved and the parking window
is returned. If no context is found (really shouldn't happen) null is
returned.

ThreadContext.FromId - This method takes
a thread id and indexes a special hash to find the context for the
given thread. If one doesn't exist then a new ThreadContext is created
and returned in it's place.

ThreadContext.FromCurrent - This method
grabs the current ThreadContext out of thread local storage. I'm
guessing this must be faster than getting the current thread id and
indexing the context hash, else why would they use thread local storage
at all?

ThreadContext..ctor() - This is the most
confusing IL to examine, but it appears the constructor does some self
registration into a context hash that the other methods use to get the
context for a given thread. They wind up using some of the Thread
methods, namely SetData, to register things into thread local storage.
Why they use thread local storage and a context hash indexed by thread
ID, I'm just not sure.

9. Conclusion
You've
learned quite a bit about the Windows Forms marshalling pump today and
how it handles all of the various methods of cross thread marshalling.
You've also gotten a peak deeper into the Windows Forms source through a
very detailed IL inspection. I've come up with some derived concepts
based on this whole process, so maybe these will lead into some even
more compelling articles. Even more importantly, we've learned how the
process can break down if we are expecting a specific order of events.

I had never fully examined this code
before, so even I was surprised at some of what I found. For instance,
the performance implications of calling the same method multiple times
asynchronously might be something that should be considered. Knowing
that all delegates will be processed in a tight loop is pretty huge and
that items can be queued while others are being dequeued (aka you can
hang yourself). Finally, the realization that if you use an
EventHandler type, you can't pass in the sender explicitly might lead to
confusion for some folks. After all, if you mock up an arguments array
and pass it to Invoke or BeginInvoke you would expect it to be used.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐