Why locking 'backgroundworker' when during 'control.Invoke()'

Viewed 69

I'm updating a textbox from the 'Backgroundworker' with the "Invoke" method. But when I "refresh" the picturebox in the ui , the 'backgroundworker' thread locks. So there is no problem of updating ui only. For this example, counter doesn't incrementing.Thanks.

int counter = 0;

private delegate void SafeCallDelegate(string text);

public Form1()
{
    InitializeComponent();
    backgroundWorker1.RunWorkerAsync();
}

private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
    Thread.Sleep(3000); //do stuff
}

private void btnRefresh_Click(object sender, EventArgs e)
{
    pictureBox1.Refresh();
}

private void UpdateTextBox(string text)
{
    if (textBox1.InvokeRequired)
    {
        var d = new SafeCallDelegate(UpdateTextBox);
        textBox1.Invoke(d, new object[] { text });
    }
    else
    {
        textBox1.Text = text;
    }
}

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    while (true)
    {
        Thread.Sleep(100);
        counter++;
        UpdateTextBox(counter.ToString());
    }
}
3 Answers

Control.Invoke will block until the call on the main thread has completed, and due to the 3s sleep in the paint event, this will take a while. If you do not want this, use .BeginInvoke, that will post the update to the main thread and return immediately.

However, the comments are correct in that async/await/Task.Run or a timer is better way to go.

The reason is because Invoke means to wait for the main UI thread to do the work before the background thread can continue.

Invoke is done using messages, as is painting.

When the background thread calls Invoke, a message is put on the message queue that the main UI thread is processing, and when it gets to that message, whatever the delegate you passed does will be done in the context of the main UI thread. While the message is being processed, the background thread will be waiting for Invoke to return, which will only happen after the message has been processed.

Now, what happens if you do a Thread.Sleep(3000) in the paint of the picturebox? The painting is also done using messages, and the message loop code looks simplified like this:

while (true)
{
    var message = WaitForAndGetNextMessage();
    ProcessMessage(message); // this will return only when message has been processed
}

So when the paint message for the picturebox arrives, and you sleep for 3 seconds inside, the message loop is not processing messages. If there is a message from the Invoke method in the queue, this will also be delayed for the same 3 seconds, and while this is delayed, your background thread is waiting for the message to be processed, which means it too will be stuck waiting for 3 seconds.

One way to "fix" this would be to use the fire-and-forget BeginInvoke instead of Invoke. This will put the message in the queue but the background thread will not wait for it to be processed and will continue immediately. This, of course, would mean that your 100ms background loop would be adding about 30 of those messages into the queue while the paintbox is being "painted" and all of those 30 messages will be processed afterwards in a short amount of time.


Would this be fixed by tasks that others have hinted at in the comments? Not if you still do something lengthy (like Thread.Sleep) in an event handler.

You should use Microsoft's Reactive Framework (aka Rx) - NuGet System.Reactive.Windows.Forms and add using System.Reactive.Linq; - then you can do this:

public Form1()
{
    InitializeComponent();
    
    Observable
        .Interval(TimeSpan.FromMilliseconds(100.0))
        .ObserveOn(this)
        .Subscribe(n => textBox1.Text = n.ToString());
}

That's it. It's a timer that fires every 100 milliseconds, it pushes the call to the current form, and then it updates the text box.

It is a lot cleaner than background workers or plain threads.

Related