Dispatcher Invoke(…) vs BeginInvoke(…) confusion

2019-01-08 14:20发布

问题:

I'm confused why I can't make this test counter application work with 2 (or more) simultaneous running countertextboxes with the use of "BeginInvoke" on my Dispatcher in the Count() method.

You can solve the issue by replacing the BeginInvoke by an Invoke. But this doesn't solve my confusion.

Here's the sample code I'm talking about:

public class CounterTextBox : TextBox
{
    private int _number;

    public void Start()
    {
        (new Action(Count)).BeginInvoke(null, null);
    }

    private void Count()
    {
        while (true)
        {
            if (_number++ > 10000) _number = 0;
            this.Dispatcher.BeginInvoke(new Action(UpdateText), System.Windows.Threading.DispatcherPriority.Background, null);    
        }
    }

    private void UpdateText()
    {
        this.Text = "" + _number;
    }
}

回答1:

When you use Dispatcher.BeginInvoke it means that it schedules the given action for execution in the UI thread at a later point in time, and then returns control to allow the current thread to continue executing. Invoke blocks the caller until the scheduled action finishes.

When you use BeginInvoke your loop is going to run super fast since BeginInvoke returns right away. This means that you're adding lot and lots of actions to the message queue. You're adding them much faster than they can actually be processed. This means that there's a long time between when you schedule a message and when it actually gets a chance to be run.

The actual action that you're running uses the field _number. But _number is being modified by the other thread very quickly and while the action is in the queue. This means that it won't display the value of _number at the time you scheduled the action, but rather what it is after it has been continuing on in it's very tight loop.

If you use Dispatcher.Invoke instead then it prevents the loop from "getting ahead of itself" and having multiple scheduled events, which ensures that the value that it's writing is always the "current" value. Additionally, by forcing each iteration of the loop to wait for the message to be run it makes the loop a lot less "tight", so it can't run as quickly in general.

If you want to use BeginInvoke the first thing you really need to do is slow down your loop. If you want it to update the text every second, or ever 10ms, or whatever, then you can use Thread.Sleep to wait the appropriate amount of time.

Next, you need to take a copy of _number before passing it to the Dispatcher so that it displays the value at the time you scheduled it, not at the time it is executed:

while (true)
{
    if (_number++ > 10000)
        _number = 0;
    int copy = _number;
    this.Dispatcher.BeginInvoke(new Action(() => UpdateText(copy))
        , System.Windows.Threading.DispatcherPriority.Background, null);
    Thread.Sleep(200);
}

private void UpdateText(int number)
{
    this.Text = number.ToString();
}