Tuesday, December 1, 2009

Threading

When VB.NET first came out I had many thoughts of going back to VB6. They seemed to have changed everything, and the VB6 to .NET converter always failed to convert my code. But then there was two things that I discovered that sold me on VB.NET.

#1) Threading
#2) Registry Access

Back in my VB6 days my team tried many times to get things to work muti-threaded, there were magazine articles at the time that claimed to be able to do it using windows API hacks, and MDI forms, but we were never able to get it to work. In .NET you just have to call .Start on a thread object is just Amazing.

Registry access in VB6 was crazy. You had to write pages and pages of code using a bunch of Win32 API Calls. In .NET I changed my pages of code into just a few lines. Once I wrote my first .NET Registry access routine I never wanted to write in VB6 again.


Here is some code that I modified that does both Registry Access and threading. I'm not sure who wrote this code originally, but I modified it so it would be thread safe. Your form needs to have the following controls:

A timer named Timer1, set to go off every 5 seconds (5000 ms)
A datagrid control named DataGridView1
Two buttons Button1 and Button2


If your are using this on a VISTA box you will need to set your UAC Settings, see this post for more information. http://vb-daryl.blogspot.com/2009/11/uac-administrator-access.html



Imports System.Threading.Thread
Imports Microsoft.Win32


Public Class Form1

  Dim dtResults As DataTable
  Dim dtFound As DataTable

  Public Shared ProcessThread As System.Threading.Thread
  Dim Path As String = "SYSTEM\\"
  Dim SearchStr As String = "USERNAME"
  Dim WaitRequest As Boolean
  Dim TS As ThreadStatus

  Enum ThreadStatus
    Running
    Wating
  End Enum

  Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    dtResults = New DataTable
    dtResults.Columns.Add("Name", GetType(String))
    dtResults.Columns.Add("Value", GetType(String))


    dtFound = New DataTable
    dtFound.Columns.Add("Name", GetType(String))
    dtFound.Columns.Add("Value", GetType(String))

    Timer1.Enabled = True

    TS = ThreadStatus.Running
    ProcessThread = New System.Threading.Thread(AddressOf SearchThread)
    ProcessThread.Start()

    '    SearchThread()
  End Sub

  Sub SearchThread()
    Search(Path, SearchStr)
    MsgBox("Search Complete")
  End Sub

  Public Sub Search(ByVal Path As String, ByVal SearchStr As String)

    If WaitRequest = True Then
      WaitRequest = False
      TS = ThreadStatus.Wating

      While TS = ThreadStatus.Wating
        System.Threading.Thread.Sleep(10)
      End While

    End If

    Dim dRow As DataRow

    Dim ParentKey As RegistryKey = Registry.LocalMachine.OpenSubKey(Path, True)
    ' Loop through values in the subkey
    For Each valueName As String In ParentKey.GetValueNames()
      On Error Resume Next
      'Create String to Compare against
      Dim CurStr As String = valueName + " = " + ParentKey.GetValue(valueName)
      Dim CurValue As String = ParentKey.GetValue(valueName)
      dRow = dtResults.NewRow
      dRow("Name") = valueName
      dRow("Value") = ParentKey.GetValue(valueName)
      dtResults.Rows.Add(dRow)

      'ListBox1.Items.Insert(0, valueName + " = " + ParentKey.GetValue(valueName))
      If CurValue = SearchStr Then
        Dim dRowF As DataRow = dtFound.NewRow()
        dRowF("Name") = valueName
        dRowF("Value") = CurValue
        dtFound.Rows.Add(dRowF)
        CurStr = ""
        CurValue = ""
      End If
    Next
    'if there are sub keys loop through and be recursive
    If ParentKey.SubKeyCount > 0 Then
      For Each subKeyName As String In ParentKey.GetSubKeyNames()

        dRow = dtResults.NewRow
        dRow("Name") = Path + subKeyName
        dRow("Value") = ""
        dtResults.Rows.Add(dRow)

        'ListBox1.Items.Insert(0, "")
        'ListBox1.Items.Insert(0, Path + subKeyName) ' Writing the Keyname
        'This is what makes me recursive!
        Dim Thispath As String = Path + subKeyName + "\\"
        Search(Thispath, SearchStr)
      Next

    End If

  End Sub

  Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick

    WaitRequest = True

    Timer1.Stop()

    While TS = ThreadStatus.Running
      System.Threading.Thread.Sleep(10)
      Application.DoEvents()
    End While

    Me.Label1.Text = "Checked " & dtResults.Rows.Count & " Rows"
    Dim dtTemp As DataTable = dtResults.Copy
    Dim dtTempF As DataTable = dtFound.Copy

    Me.DataGridView1.DataSource = dtTemp
    Me.DataGridView2.DataSource = dtTempF

    TS = ThreadStatus.Running

    Timer1.Start()
    Dim x As DataView = New DataView(dtTemp)
    x.Sort = "idOrder, Name"

  End Sub

  Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
    ProcessThread.Abort()
    ProcessThread = Nothing
  End Sub
End Class

This code does have a bug that I didn't take the time to figure out, after it searches 200,000 or so keys it starts crashing. I'm not sure why, my guess is it is trying to access some System Protected Key, but this code works well enough that I am not going to worry about that.

Now that you've seen that I would like to discuss some of the concepts that you need to be aware of in Threading.

Mainly "Thread Safety" and "Thread Management".

"Thread Safety"

Most people new to threading do not think much about this, you spawn some thread and want to post it's results back to the UI.  But you will find when you code tries to access a datagrid, or listbox that is on the UI thread that it will throw an error as a background thread will not be allowed to mess with the UI thread. There are a number of solutions to this, I generally use the Singleton pattern to create a buffer to hold data in.  I'll talk more about Singletons in later posts.  For this example I decided to use the timer control to pause the background thread, and update the UI with it's current results.

"Thread Management"

This concept is important as well. If you do not stop your other threads when your application is shut down they will keep running forever.  You also need to be concerned about how many threads you create. The first time I wrote a threading application I wrote a program which searched all files and folders on a hard drive. The main thread searched for new folders, and it spawned another thread to search the files. As it ran it just kept spawning thread after thread until it hit around 32,000 threads at which time my computer, without giving an error message, turned itself off.  In this example we only create one thread so you need not worry about it crashing you computer.

I generally create my own threading class which spawns a set number of threads (usually in it's constructor) and manages them.  It keeps a Queue of new things to spawn threads for, and then checks the status of each thread, once the thread is done with it's task the threading class records the results, disposes the thread and creates a new thread with the next task in it's Queue.  I believe this is called "Thread Polling" but I don't know I created this method on my own without reading about it.

No comments:

Post a Comment