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

 

国内私募机构九鼎控股打造APP,来就送 20元现金领取地址: http://jdb.jiudingcapital.com/phone.html
内部邀请码: C8E245J (不写邀请码,没有现金送)
国内私募机构九鼎控股打造,九鼎投资是在全国股份转让系统挂牌的公众公司,股票代码为430719,为“中国PE第一股”,市值超1000亿元。 

 

------------------------------------------------------------------------------------------------------------------------------------------------------------------

 

 

 

Abstract:
Marshalling [d1] 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 [d2] 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 [d3] 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:

    1. UCS 1: Using InvokeRequired and Invoke for Synchronous Marshalling, the default scenario
    2. UCS 2: Using BeginInvoke for Asynchronous Marshalling
    3. InvokeRequired and how it works
    4. Invoke operation on the UI thread and from a different thread
    5. InvokeMarshaledCallbacks and how it handles the callback queue
    6. BeginInvoke operation on the UI thread and from a different thread
    7. UCS 3: Using BeginInvoke to change a property after other events are processed, and why it can fail
    8. Public and Internal Methods covered with a short description of what they do
    9. 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 [d4] 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.

Published Wednesday, May 05, 2004 3:00 AM by Justin Rogers

Filed under: WinForms

Comments

Tuesday, May 25, 2004 5:59 AM by Matt

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Very interesting article, Thanx !

Friday, May 28, 2004 1:09 PM by Cepheus

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Thanks for an excellent article covering some very much needed information for me. I've been working on a multithreaded network application and I've been stressing over how to deal with the UI, and the methods you examined here have solved it for me. :) Thanks a bunch!

Thursday, June 03, 2004 8:11 PM by Tandem_Guru

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

I think this is THE most detailed description regarding Winform pitfalls that most likely confuse a lot of newbies like me. 

Thanks for the clarification! 

How about another paper regarding UIThread and other background thread so that it could really complete the whole topic? 


Thanks again! 

Saturday, June 05, 2004 7:24 AM by Ole Lytjohan

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Nice article, but you should really make some people read through it first before posting it. It's filled with errors :/ 

It's not a big thing, the article by itself is nice, but it would just have been much better, if those issues where fixed.

Saturday, June 05, 2004 7:40 AM by Justin Rogers

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Would be nice if you pointed out the errors so they could be fixed. Up to now I've had many people read through it and not point out any errors. 

If you are speaking of spelling mistakes or grammar issues then I won't even bother, but true errors in function are definitely something I can post addendums for.

Saturday, June 05, 2004 7:42 AM by Ole Lytjohan

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Previous post of mine was in error. No errors. 
Read some of it wrong, just shows :)

Saturday, June 05, 2004 7:42 AM by Justin Rogers

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Tandem: What exactly are you talking about with the extension paper you propose? Are you thinking of an additional use case scenario that perhaps I've missed or didn't fully explain so that you could apply it to your problem?

Saturday, June 05, 2004 8:18 AM by Justin Rogers

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Ole: Hey, no worries. I've definitely posted mistakes before and been called for it, so I'm always curious what I'm going to get in return after a posting. Thanks for reading it so closely.

Thursday, June 10, 2004 10:23 AM by Greg Knierim

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Excellent article...However, not ever using Thread Invokes, I must ask can someone give a good scenario of when I would use this architecture?

Wednesday, June 23, 2004 5:59 PM by TrackBack

# Background Processing

Friday, July 09, 2004 12:09 AM by Darren Neimke

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Greg, a common example might be if you wanted to update some UI off the back of the Timers.Timer elapsed event because the timer is running off on a different thread you will need to Invoke back onto the main thread before touching the UI.

Friday, July 09, 2004 12:12 AM by Darren Neimke

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Justin, 

Great article! 

The one thing that I didn't quite get was in section 4 when you state: 

**************************** 
If we didn't make this check and instead just started a WaitOne on the handle, we'd hang indefinitely. 
**************************** 

Why? Are you saying that, under that circumstance you'd be guaranteed to hang indefinitely? Don't quite get that I'm sorry.

Saturday, July 31, 2004 2:49 PM by Joel Moore

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

I glad I found this. Although much of the article went over my head, I learned enough from it to fix a bug that's been hassling me for some time now (showing a hidden window from a worker thread). Now I realize I'm going to have to buckle down and learn a lot more about multithreading in .NET before using it like I am. After I've done some learnin' I'll come back and read this again to see if more of it soaks in. 

Thanks for saving my hide in the meantime.

Wednesday, August 04, 2004 1:37 AM by Mike Marinich

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Justin, 
Thanks for the most detailed info on the topic I could find up to date (before running ILDASM) 

I'd like to get some clarification on how the messages that result from calling Invoke/BeginInvoke synch up with user generated UI messages like mouse clicks or key strokes. 

In the #6 you state that BeginInvoke results in a message post to the window pump. In this case any mouse click should get posted before or after my message and it’s processed without interfering with my delegate. I'm not certain whether the Invoke also ends up with a message in the window queue. 

My UI is updated from multiple threads alone with the user actively interacting with it. The way to make sure the thread generated updates are sequential with the windows event (mouse/key) processing is to ensure the thread calls are posted to the same queue where windows messages are posted to. 

If you have any suggestions, any other links I can gather more info from, I'd greatly appreciate it. 

Thanks 

My email is mike@crediware.com

Wednesday, August 04, 2004 4:02 AM by Justin Rogers

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Mike, I recommend reading section 7 very thoroughly. All BeginInvoke calls are processed when the very first message gets through the pump. This means they are all batched and processed at the same time. What can this mean for Mouse events? 

Well, say you send a BeginInvoke (PostMessage), and the user clicks the mouse, and then you call another BeginInvoke. 

When the UI thread kicks in, it will process the very first message and execute BOTH delegates, and then process the mouse. If you expected your delegate to occur after the mouse event then you'll need to come up with a better way to do invokes that links each delegate to the message that was sent to start it.

Thursday, August 12, 2004 1:09 PM by TrackBack

# UI Update from threads, interesting Blog.

Tuesday, September 21, 2004 9:35 PM by TrackBack

# re: Quand Microsoft tente de nous remettre dans le droit chemin

Thursday, September 23, 2004 10:35 AM by TrackBack

# Update on the Windows Forms Delayed Handle Creation Bug

I got an email about a week from a fellow called Jeff Berkowitz who had experienced the same problem with the delayed window handle creation that I posted on here. Jeff's post of 9 Aug 2003 describes his discovery of the problem, and in his email he references this incredible...

Thursday, September 23, 2004 10:37 AM by TrackBack

# Update on the Windows Forms Delayed Handle Creation Bug

Monday, October 04, 2004 4:23 AM by TrackBack

# Great article on WinForm UI thread invokes

Monday, November 01, 2004 5:59 AM by TrackBack

# 在多线程中如何调用Winform

Ping Back来自:blog.csdn.net

Wednesday, November 24, 2004 8:17 PM by TrackBack

# All about windows handles

Friday, January 07, 2005 6:04 AM by TrackBack

# Handling exceptions in a worker thread

This post explains how to process the details on your windows form of an exception thrown from a worker thread. Basically I raise an event to the client application and passing the Exception object with it. The call of the event is marshalled using the clients invoke method.

Thursday, February 03, 2005 9:25 PM by TrackBack

# Thread Marshalling

Thursday, May 12, 2005 4:30 PM by TrackBack

# Whidbey help for multithreading woes: CheckForIllegalCrossThreadCalls

A somewhat common programming error is to directly call back onto a piece of UI when you’re on the wrong...

Monday, June 04, 2007 3:24 AM by Agnel CJ Kurian

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Just curious... Supposimg I need to invoke several methods with no parameters, would it be more efficient to use the old-style SendMessage or PostMessage via PInvoke? I am not discussing safety here only efficiency. Any thoughts?

Thursday, November 01, 2007 9:24 AM by Miron

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Very nice,and well written article.

Thanks

Tuesday, November 06, 2007 4:09 AM by Simonjohn Roberts

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

some 'using' declarations would have been nice...

excellent article otherwise.

Monday, February 25, 2008 9:07 PM by Sean Rhone

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

I'm fairly new to async programming and I've got to say thanks for this. It is well written and very detailed.

Saturday, April 05, 2008 7:49 PM by MANSOUR

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

I use this code before but i dont understand , but when i read this article i really understand invoke .

thank you.

Wednesday, April 30, 2008 3:12 AM by Martin Schmid

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Hi, I'm really curious: I call Invoke from a thread off the GUI and it sends registred message, so what prevents me from sending this faked message from my app running under lower privilegies so as to run the code under admin?

Thanks a lot, I'm anxious

Monday, July 14, 2008 8:39 PM by Lexapro.

# Lexapro 10mg.

Half life of lexapro. Lexapro.

Monday, August 18, 2008 10:45 AM by Dnet

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Hi,

My problem is being able to update UI on a form from a worker thread spawned from another class. I have been struggling with that. Please help.

Thanks

Friday, December 26, 2008 3:47 AM by mono

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Hi Justin,

Thanks for this great article. It really helped me a lot.

Cheers,

Mono

Thursday, January 08, 2009 2:56 PM by S. Floyd

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Thanks for taking the time to describe your interpretation of the ILDASM code. It is good to know about the degenerate case from a performance perspective.

Tuesday, February 17, 2009 10:05 PM by Inenceexhance

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Hello My name is Bob

Does anyone know anything about blackhat stuff? I found these sites and these people are have proof of making over $900 a day from some these Black hat methods. What do you guys think about this.

<a href=http://www.moneymakerdiscussion.com>  Click Here Now To Learn More! Blackhat seo</a>

<a href=http://www.myfreemoneyforum.com>  Or Go Here! e-currency</a>

Tuesday, September 22, 2009 3:18 AM by sweerbhob

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Hi,

it's pride and glory to develop first webpage using own hands :)

What do you think?

http://www.sweerbhob.net

Cheers!

Tuesday, October 13, 2009 7:26 PM by Karl

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Hi,

I have a question.

If I don't check InvokeRequired and always use Invoke to call delegate. Will this cause any problem?

thanks,

Friday, October 23, 2009 1:03 AM by vcdebugger

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

Hi,

Nice informative article but really very confusing for the newbies !!

Thursday, January 07, 2010 3:16 AM by Sean

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

The following part of the text needs correcting (change myControl.Hide to myControl.Focus);

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());

Monday, February 01, 2010 6:25 AM by Benjamin

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

This is probably the best article I came across on UI updates in multiple threads.

It helped a lot.

Thanks!

Thursday, April 01, 2010 5:20 AM by KenHeagekem

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

brazil.mcneel.com/.../lopid.aspx lopid

Wednesday, May 05, 2010 11:34 AM by deephotadia

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

DUBLIN (Reuters) – The Irish <a href=http://www.redcarpetflorist.com>Online Flower Delivery</a> Aviation Word said it would undergo flights to take up again from all Irish airports from 1200 GMT on Tuesday but volcanic ash could create on more disruptions later in the week and periodically from everyone close to the other of the summer.

The IAA had closed airports from 0600 GMT until 1200 GMT correct to play of ash ingestion in aircraft engines, although overflights of Ireland from Britain and continental Europe had not been banned.

Thursday, May 13, 2010 10:03 AM by twoddle

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

this is nice syntax:

// Delegate

public delegate void Action();

// Convenience method:

void InvokeOnDemand(Action dg) {

if (InvokeRequired) {

Invoke(dg);

} else {

dg();

}//if

}//method

// Usage:

public void SetMessage(string Message) {

InvokeOnDemand(delegate() {

txtMessage.Text = Message;

Clock.Enabled = false;

progBar.Value = progBar.Maximum;

});//delegate

}//method

Saturday, August 07, 2010 5:02 PM by Infragistics WinForms Controls inside of WPF Applications | dandesousa.com

# Infragistics WinForms Controls inside of WPF Applications | dandesousa.com

Pingback from  Infragistics WinForms Controls inside of WPF Applications | dandesousa.com

Sunday, August 22, 2010 12:30 AM by AnitotrartMaw

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

I enjoyed reading your blog. Keep it that way.

Sunday, September 05, 2010 3:21 PM by thrhtrurth

# re: WinForms UI Thread Invokes: An In-Depth Review of Invoke/BeginInvoke/InvokeRequred

grynrzirymuygovetwasv. <a href=www.acnetreatment2k.com/>acne treatment</a>

erwktx


 [d1]封送处理

 [d2]境况、情况、情景

 [d3]提高

 [d4]显著的, 突出的

转载于:https://www.cnblogs.com/AloneSword/archive/2010/09/21/1832514.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。
这个异常通常是因为在非UI线程上尝试更新UI控件的值而导致的。在 WinForms 中,UI控件只能在创建它们的UI线程上访问和更新。 要解决这个问题,你可以使用 Control.Invoke 或 Control.BeginInvoke 方法来在UI线程上执行更新操作。以下是一个示例代码,展示了如何使用 Invoke 方法来解决这个问题: ```csharp using System; using System.Threading; using System.Windows.Forms; public class Program { private static ProgressBar progressBar; public static void Main() { Form form = new Form(); progressBar = new ProgressBar(); form.Controls.Add(progressBar); // 创建一个线程来模拟任务的进度更新 Thread thread = new Thread(UpdateProgress); thread.Start(); // 显示窗体 Application.Run(form); } private static void UpdateProgress() { for (int i = 0; i <= 100; i++) { // 在 UI 线程中更新进度条的值 progressBar.Invoke(new Action(() => progressBar.Value = i)); // 模拟任务的延迟 Thread.Sleep(100); } } } ``` 在上述代码中,我们使用了 Invoke 方法来确保在 UI 线程上更新进度条的值。这样可以避免 InvalidOperationException 异常。 注意,如果你不需要等待UI线程完成操作并返回结果,你也可以使用 Control.BeginInvoke 方法。它与 Invoke 方法类似,但是它是异步执行的,不会阻塞当前线程。 通过使用 Invoke 或 BeginInvoke 方法,你可以确保在正确的线程上更新UI控件的值,并避免出现 InvalidOperationException 异常。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值