Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A threading problem where mono hangs and MS.Net doesn't

I'm testing my app with mono in prevision of a Linux port, and I have a threading problem. I initially considered pasting 3000 code lines here, but finally I've devised a small minimal example ;)

You have a form with a button (poetically named Button1, and a label (which bears, without surprise, the name Label1)). The whole lot is living a happy life on a form called Form1. Clicking Button1 launches an infinite loop that increments a local counter and updates Label1 (using Invoke) to reflect its value.

Now in Mono, if you resize the form, the label stops updating, never to restart. This doesn't happen with MS implementation. BeginInvoke doesn't work any better; worse, it makes the UI hang in both cases.

Do you know where this discrepancy comes from? How would you solve it? And finally, why doesn't BeginInvoke work here? I must be making a huge mistake... but which?


EDIT: Some progress so far:
  • Calling BeginInvoke does in fact work; only, the UI just doesn't refresh fast enough, so it seems to stop.
  • On mono, what happens is that the whole thread hangs when you insert a message in the UI queue (eg by resizing the form). In fact, the synchronous Invoke call never returns. I'm trying to understand why.
  • Of interest: even using BeginInvoke, the asynchronous calls don't get executed before the resizing operation ends. On MS.Net, they keep running while resizing.

The code looks like this (C# version lower):

Public Class Form1
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Dim T As New Threading.Thread(AddressOf Increment)
        T.Start()
    End Sub

    Sub UpdateLabel(ByVal Text As String)
        Label1.Text = Text
    End Sub

    Delegate Sub UpdateLabelHandler(ByVal Text As String)
    Sub Increment()
        Dim i As Long = 0
        Dim UpdateLabelDelegate As New UpdateLabelHandler(AddressOf UpdateLabel)
        Try
            While True
                i = (i + 1) Mod (Long.MaxValue - 1)
                Me.Invoke(UpdateLabelDelegate, New Object() {i.ToString})
            End While
        Catch Ex As ObjectDisposedException
        End Try
    End Sub
End Class

Or, in C#,

public class Form1
{
    private void Button1_Click(System.Object sender, System.EventArgs e)
    {
        System.Threading.Thread T = new System.Threading.Thread(Increment);
        T.Start();
    }

    public void UpdateLabel(string Text)
    {
        Label1.Text = Text;
    }

    public delegate void UpdateLabelHandler(string Text);
    public void Increment()
    {
        long i = 0;
        UpdateLabelHandler UpdateLabelDelegate = new UpdateLabelHandler(UpdateLabel);
        try {
            while (true) {
                i = (i + 1) % (long.MaxValue - 1);
                this.Invoke(UpdateLabelDelegate, new object[] { i.ToString() });
            }
        } catch (ObjectDisposedException Ex) {
        }
    }
}
like image 543
Clément Avatar asked Apr 27 '11 12:04

Clément


2 Answers

This is a bug in the mono runtime, at least I think it is. The code might not be good practice (I'm not a threading expert), but the thing that suggests a bug is the fact that the behaviour differs on windows and Linux.

On Linux, mono has exactly the same behaviour as MS.Net has on windows. No hanging, continuous updates even while resizing.

On Windows, mono displays all the aforementioned problems. I've posted a bug report at https://bugzilla.novell.com/show_bug.cgi?id=690400 .

like image 99
Clément Avatar answered Oct 30 '22 20:10

Clément


Do you know where this discrepancy comes from? How would you solve it?

I am not sure. I do not see anything obvious in your code that would cause the difference between Mono and .NET. If I had to make a wild guess I would say there is a possibility that you have stumbled upon an obscure bug in Mono. Though, I suppose it is possible that Mono uses a sufficiently different mechanism for handling the WM_PAINT messages that cause the form to get refreshed. The constant pounding of the UI thread from repeated calls to Invoke may be disrupting Mono's ability to get the form refreshed.

And finally, why doesn't BeginInvoke work here?

Calling Invoke in a tight loop is bad enough, but BeginInvoke will be even worse. The worker thread is flooding the UI message pump. BeginInvoke does not wait until the UI thread has finished executing the delegate. It just posts the requests and returns quickly. That is why it appears to hang. The messages that BeginInvoke is posting to the UI message queue keep building up as the worker thread is likely severely out pacing the UI thread's ability to process them.

Other Comments

I should also mention that the worker thread is nearly useless in the code. The reason is because you have a call to Invoke on every iteration. Invoke blocks until the UI has finished executing the delegate. That means your worker thread and UI thread are essentially in lock-step with each other. In other words, the worker is spending most of its time waiting for the UI and vice versa.

Solution

One possible fix is to slow down the rate at which Invoke is called. Instead of calling it on every loop iteration try doing it every 1000 iterations or the like.

Any even better approach is to not use Invoke or BeginInvoke at all. Personally, I think these mechanisms for updating the UI are way overused. It is almost always better to let the UI thread throttle its own update rate especially when the worker thread is doing continuous processing. This means you will need to place a timer on the form and have it tick at the desired refresh rate. From the Tick event you will probe a shared data structure that the worker thread is updating and use that information to update the controls on the form. This has several advantages.

  • It breaks the tight coupling between the UI and worker threads that Control.Invoke imposes.
  • It puts the responsibility of updating the UI thread on the UI thread where it should belong anyway.
  • The UI thread gets to dictate when and how often the update should take place.
  • There is no risk of the UI message pump being overrun as would be the case with the marshaling techniques initiated by the worker thread.
  • The worker thread does not have to wait for an acknowledgement that the update was performed before proceeding with its next steps (ie. you get more throughput on both the UI and worker threads).
like image 1
Brian Gideon Avatar answered Oct 30 '22 19:10

Brian Gideon