user6493962
user6493962

Reputation:

vb.net delegate and invoke - Multithread

I am trying to understand how delegates and invoke work.

So I build a form, with a label and a button.

When someone clicks on the Button, the Text changes to "Stop" and a counter starts counting up. This counter should be displayed on a Label. This is my code:

Public Class Form1

  Private t1 As Thread
  Private sek_ As Integer = 0

  Private Sub btn_read_Click(sender As Object, e As EventArgs) Handles btn_read.Click


    If btn_read.Text = "Read" Then

        btn_read.Text = "Stop"
        t1 = New Thread(AddressOf stopw_)
        t1.Start()

    Else
        lbl_stopw_.Text = ""
        btn_read.Text = "Read"

    End If

  End Sub

  Private Delegate Sub stopw_D()

  Private Sub stopw_()

    Do While btn_read.Text = "Stop"
        sek_ = sek_ + 1

        If lbl_stopw_.InvokeRequired Then
            lbl_stopw_.Invoke(New stopw_D(AddressOf stopw_))
        Else
            lbl_stopw_.Text = sek_
        End If

        Thread.Sleep(1000)

    Loop

    sek_ = 0

    If t1.IsAlive Then t1.Abort()

  End Sub

End Class

If I start debugging, the form still freezes and the label does not get updated. If I delete all the delegate and invoke stuff and use Me.CheckForIllegalCrossThreadCalls = False Its working.

What am I doing wrong?

Upvotes: 1

Views: 3764

Answers (1)

Visual Vincent
Visual Vincent

Reputation: 18310

Control.Invoke() is used to execute a method on the same thread that the control was created on. Since code can only be executed one line at a time in each thread, executing a method on the control's thread will result in the method being "queued" until the other code in that thread, prior to the method, has completed. This makes it thread-safe as there will be no concurrency issues.

A Delegate is simply a class holding the pointer to a method. It exists so that you can use methods as if they were ordinary objects (in this case you pass it to a function). The AddressOf operator is a quick way of creating a delegate.

Now, you have a few issues in your code. First of all, you should not try to access or modify ANY UI element from a background thread. Whenever you want modify or check a control you must always invoke.

More specifically, I'm talking about your While-loop:

'You can't check the button here without invoking.
Do While btn_read.Text = "Stop"

It is better if you create a Boolean variable that indicates when the thread should run.

Private t1 As Thread
Private sek_ As Integer = 0
Private ThreadActive As Boolean = False

Set ThreadActive to True before you start the thead, then in your thread's While-loop check:

Do While ThreadActive

Now, there is another issue. Your UI freezes because of this:

If lbl_stopw_.InvokeRequired Then
    lbl_stopw_.Invoke(New stopw_D(AddressOf stopw_))

NEVER invoke the the same method which the thread runs on! Doing so starts the processing all over again, but on the UI thread. Your loop makes the UI thread completely busy, which is why it doesn't redraw itself.

So when you are to update something, always invoke a seperate method. If you target .NET 4.0 or higher you can use lambda expressions for a quick, inline delegate:

If lbl_stopw_.InvokeRequired Then
    lbl_stopw_.Invoke( _
        Sub()
            lbl_stopw_.Text = sek_
        End Sub)
Else
    lbl_stopw_.Text = sek_
End If

However if you are targeting .NET 3.5 or lower you have to stick to the normal way of using delegates:

'Outside your thread.
Private Delegate Sub UpdateLabelDelegate(ByVal Text As String)

Private Sub UpdateLabel(ByVal Text As String)
    lbl_stopw_.Text = Text
End Sub

'In your thread.
If lbl_stopw_.InvokeRequired Then
    lbl_stopw_.Invoke(New UpdateLabelDelegate(AddressOf UpdateLabel), sek_)
Else
    UpdateLabel(sek_)
End If

Alternatively, in order to minimize the amount of code you have to write you can create an extension method to do the invoking for you:

Imports System.Runtime.CompilerServices

Public Module Extensions
    <Extension()> _
    Public Sub InvokeIfRequired(ByVal Control As Control, ByVal Method As [Delegate], ByVal ParamArray Parameters As Object())
        If Parameters Is Nothing OrElse _
            Parameters.Length = 0 Then Parameters = Nothing 'If Parameters is null or has a length of zero then no parameters should be passed.
        If Control.InvokeRequired = True Then
            Control.Invoke(Method, Parameters)
        Else
            Method.DynamicInvoke(Parameters)
        End If
    End Sub
End Module

Usage, .NET 4.0 or higher:

lbl_stopw_.InvokeIfRequired( _
    Sub()
        lbl_stopw_.Text = sek_
    End Sub)

Usage, .NET 3.5 or lower:

lbl_stopw_.InvokeIfRequired(New UpdateLabelDelegate(AddressOf UpdateLabel), sek_)

When using this extension method you don't need to write InvokeRequired checks everywhere:

Do While btn_read.Text = "Stop"
    sek_ = sek_ + 1

    lbl_stopw_.InvokeIfRequired(New UpdateLabelDelegate(AddressOf UpdateLabel), sek_)

    Thread.Sleep(1000)
Loop

And finally, this is just unnecessary:

If t1.IsAlive Then t1.Abort()

The thread will always be alive when it reaches that If-statement since it hasn't exited the stopw_ method yet. But once the thread has exited the method it will end normally, so there's no reason to call Abort().

The answer got a bit long, but I hope it to be helpful!

Upvotes: 4

Related Questions