Gavin Potter
Gavin Potter

Reputation: 43

Update UI thread in winforms application reliably

I have a long running machine learning program that I run in the background in parallel using all cores in a winforms application. Periodically I update the UI to report on progress.

The machine seems to choose fairly random times to execute the message pump on the UI thread. Sometimes I get no updates for several minutes, sometimes I get a message every time I send one.

I've tried every which way to make this reliable including standard invoking from the background thread, using progress reporting from a backgroundworker, using a timer on the UI thread to collect information and display it, reducing the maximum number of threads that can run in parallel, messing with thread priorities etc. The only way I've found to reliably get an update is to add a console to the winforms program and output progress to the console. For some reason, that is 100% reliable, but it is a real hack and looks messy.

Does anyone know of a way to force the ui thread to be updated reliably?

As requested: Here is the most basic code that replicates the error. Create a form with a label called label1. The code is an attempt to update the label every millionth iteration.

Module testmodule
delegate sub invokedelegate(txt as string)

Sub long_running_process()
    Dim x(100000000) As Integer
    Dim cnt As Integer
    Dim syncobject As New Object

    form1.Label1.Text = "started"

    Parallel.ForEach(x, Sub(z)

                            '*** This just put in to make the processors do some work.
                            Dim p As New Random
                            Dim m As Double = p.NextDouble
                            Dim zzz As Double = Math.Cosh(m) + Math.Cos(m)

                            '*** This is the basic updating method.
                            SyncLock syncobject
                                cnt += 1

                                '*** Update every millionth iteration
                                If cnt Mod 1000000 < 1 Then

                                    '**** This is how it is marshalled to the UI thead.
                                    If Form1.InvokeRequired Then
                                        Form1.BeginInvoke(New invokedelegate(AddressOf invokemethod), {cnt})
                                    Else
                                        Form1.Label1.Text = cnt
                                    End If

                                End If
                            End SyncLock
                        End Sub)

    Form1.Label1.Text = "Finished"
End Sub
Sub invokemethod(txt As String)
    form1.Label1.Text = txt
End Sub
end module

Upvotes: 3

Views: 396

Answers (1)

rene
rene

Reputation: 42494

The problem is that the Parallel.ForEach insists on running the all tasks on the UI thread. One way I got this changed was by calling the long_running_process from a BackgroundWorker.DoWork, like so:

Public Class Form1

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        BackgroundWorker1.RunWorkerAsync()

    End Sub

    Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
        testmodule.long_running_process(Label1)
    End Sub
End Class

Notice that I had to provide the Label1 as a parameter because otherwise the InvokeRequired always returned false.

The changed long_running_process looks like this:

Sub long_running_process(lbl As Label)
    Dim x(100000000) As Integer
    Dim cnt As Integer
    Dim syncobject As New Object

    If lbl.InvokeRequired Then
        lbl.BeginInvoke(New MethodInvoker(Sub()
                                              lbl.Text = "started"
                                          End Sub), Nothing)
    End If


    Parallel.ForEach(x, Sub(z)

                            '*** This just put in to make the processors do some work.
                            Dim p As New Random
                            Dim m As Double = p.NextDouble
                            Dim zzz As Double = Math.Cosh(m) + Math.Cos(m)

                            '*** This is the basic updating method.
                            SyncLock syncobject
                                cnt += 1

                                '*** Update every millionth iteration
                                If cnt Mod 1000000 < 1 Then

                                    '**** This is how it is marshalled to the UI thead.
                                    If lbl.InvokeRequired Then
                                        lbl.BeginInvoke(New invokedelegate(AddressOf invokemethod), {cnt.ToString()})
                                    Else
                                        lbl.Text = cnt
                                    End If

                                End If
                            End SyncLock
                        End Sub)

    If lbl.InvokeRequired Then
        lbl.BeginInvoke(New MethodInvoker(Sub()
                                              lbl.Text = "Finished"
                                          End Sub), Nothing)
    End If

End Sub
Sub invokemethod(txt As String)
    Form1.Label1.Text = txt
End Sub

The counter updates smoothly with these changes.

Upvotes: 2

Related Questions