Introduction

So this week I discovered I did a really stupid thing and this seems to happen on a regular basis to. It usually involves threads and the UI. Let me try to explain how stupid I was.

The beginning bit

Every story has a beginning and an end and you usually start of in the beginning without threads.

Let’s say I have 2 timer processes I want to run and I want to show the output of those processes on a form. But one of those processes takes a while to process, but not always, just sometimes.

Something like this.

Imports System.Threading

Public Class Form2

    Private WithEvents _t1 As New System.Windows.Forms.Timer
    Private WithEvents _t2 As New System.Windows.Forms.Timer

    Private Sub Form2_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
        _t1.Interval = 1000
        _t2.Interval = 2000
        _t1.Start()
        _t2.Start()
    End Sub

    Dim _random As New System.Random(CType(System.DateTime.Now.Ticks Mod System.Int32.MaxValue, Integer))

    Private Sub T1Tick(sender As Object, e As EventArgs) Handles _t1.Tick
        Dim rnd = _random.Next(0, 5) * 100
        Thread.Sleep(rnd)
        TextBox1.Text = DateTime.UtcNow.ToString()
    End Sub

    Private Sub T2Tick(sender As Object, e As EventArgs) Handles _t2.Tick
        TextBox2.Text = DateTime.UtcNow.ToString()
    End Sub

End Class

Of course the UI will be completely unresponsive when we run this and will only show something every so often. The solution of course is to use threads. And in particular the Threading.Timer.

The middle bit

Imports System.Threading

Public Class Form2

    Private _t1 As Timer
    Private _t2 As Timer

    Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
        _t1 = New Timer(AddressOf T1Tick, Nothing, 0, 1000)
        _t2 = New Timer(AddressOf T2Tick, Nothing, 0, 200)
    End Sub

    Private Sub T2Tick(sender As Object)
        TextBox2.Text = DateTime.UtcNow.ToString()
    End Sub

    Dim _random As New System.Random(CType(System.DateTime.Now.Ticks Mod System.Int32.MaxValue, Integer))

    Private Sub T1Tick(sender As Object)
        Dim rnd = _random.Next(0, 5) * 100
        Thread.Sleep(rnd)
        TextBox1.Text = DateTime.UtcNow.ToString()
    End Sub

End Class

Of course the above won’t run. Why not? Because of the dreaded CrossThreadMessagingException.And we all know how to solve that very easily.

Imports System.Threading

Public Class Form2

    Private _t1 As Timer
    Private _t2 As Timer

    Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
        _t1 = New Timer(AddressOf T1Tick, Nothing, 0, 1000)
        _t2 = New Timer(AddressOf T2Tick, Nothing, 0, 200)
    End Sub

    Private Delegate Sub TickDelegate(sender As Object)

    Private Sub T2Tick(sender As Object)
        If InvokeRequired Then
            Invoke(New TickDelegate(AddressOf T2Tick), sender)
        Else
            TextBox2.Text = DateTime.UtcNow.ToString()
        End If
    End Sub

    Dim _random As New System.Random(CType(System.DateTime.Now.Ticks Mod System.Int32.MaxValue, Integer))

    Private Sub T1Tick(sender As Object)
        If InvokeRequired Then
            Invoke(New TickDelegate(AddressOf T1Tick), sender)
        Else
            Dim rnd = _random.Next(0, 5) * 100
            Thread.Sleep(rnd)
            TextBox1.Text = DateTime.UtcNow.ToString()
        End If
    End Sub

End Class

The above will kind of work. You will see the timers tick in a more smooth manner. But you will also note that sometimes it doesn’t run as smoothly as you would think. And that’s because we are doing it wrong. We are in fact just faking it. It just looks like we are using 3 threads (2 timers and the UI thread) but we are merging everything on the UI thread, so the UI thread is still blocking everything.

This is a very stupid mistake to make, and I admit I did that.

The end bit

In the end all you have to is to make sure that you keep the processes that merge with the UI thread as short as possible. The merging with the UI thread is the bit where you say invoke. That is the part where your other thread gets merged into the UI thread and they are more or less one and all your code is executed (and blocking) on that thread. So a simple change to the following code is all you need.

Imports System.Threading

Public Class Form1

    Private _t1 As Timer
    Private _t2 As Timer

    Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
        _t1 = New Timer(AddressOf T1Tick, Nothing, 5000, 1000)
        _t2 = New Timer(AddressOf T2Tick, Nothing, 0, 500)
    End Sub

    Private Delegate Sub TickDelegate(sender As Object)

    Private Sub T2Tick(sender As Object)
        If InvokeRequired Then
            Invoke(New TickDelegate(AddressOf T2Tick), sender)
        Else
            TextBox2.Text = DateTime.UtcNow.ToString()
        End If
    End Sub

    Dim _random As New System.Random(CType(System.DateTime.Now.Ticks Mod System.Int32.MaxValue, Integer))

    Private Sub T1Tick(sender As Object)
        Dim ti = DateTime.UtcNow.ToString()
        Dim rnd = _random.Next(0, 5) * 100
        Thread.Sleep(rnd)
        c_OnTimeCHanged(ti)
    End Sub

    Private Delegate Sub cDelegate(sender As String)

    Private Sub c_OnTimeCHanged(T As String)
        If InvokeRequired Then
            Invoke(New cDelegate(AddressOf c_OnTimeCHanged), T)
        Else
            TextBox1.Text = T
        End If
    End Sub

End Class

Conclusion

Yep, sometimes you have to think before you do.