' Author: David Burkley
'   Date: January of 2009
'
' I created this example to demonstrate a couple of different things about threads.
' Such as...
'  a) to show you how to create a thread
'  b) to show you that you can pass a "simple" parameter to a thread when it's started
'  c) to show you that your thread can access "globally" declared variables
'  d) to show you how you can watch for the completion of a thread
'  e) to show you there are some things that you should be aware of when using threads
'  f) to show you that using two threads that are doing the same thing... doesn't save time
' In this example... I'm essentially using threads, to run one or two subroutines
' at the same time, to do the same thing. Which is... fill a globally declared array.
' You can run or do virtually anything you want within a thread.

$Optimize ON
$Option EXPLICIT
$Include "RapidQ.inc"

Type PROCESSOR_DATA
    Initial As Long
    Final As Long
End Type

Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (hWnd As Long, nIndex As Long, dwNewLong As Long) As Long

Declare Function CreateThread Lib "kernel32" Alias "CreateThread" (lpThreadAttributes As Long, dwStackSize As Long, lpStartAddress As Long, lpParameter As Long, dwCreationFlags As Long, lpThreadId As Long) As Long
Declare Function TerminateThread Lib "kernel32" Alias "TerminateThread" (hThread As Long, dwExitCode As Long) As Long
Declare Function CloseHandle Lib "kernel32" Alias "CloseHandle" (hObject As Long) As Long
Declare Sub ExitThread Lib "kernel32" Alias "ExitThread" (ByVal dwExitCode As Long)
Declare Function SetThreadPriority Lib "kernel32" Alias "SetThreadPriority" (ByVal hThread As Long, ByVal nPriority As Long) As Long

Declare Sub MainForm_OnClose
Declare Sub Start_OnClick(Sender As QMenuItem)
Declare Sub Results_OnClick

Declare Sub RunSub1(lpToPD1 As Long)
Declare Sub RunSub2(lpToPD2 As Long)

Declare Sub ThreadCheck

Declare Sub DisableMenus
Declare Sub EnableMenus

Const GWL_HWNDPARENT = (-8)
Const HWND_DESKTOP = 0
Const THREAD_BASE_PRIORITY_MIN = -2

'These type structures (aka UDTs) will be
'used as a parameter that the CreateThread
'can pass to the thread. They contain very
'simple data types. Dealing with a complex
'data type such as an array (within the UDT)
'might be challenging to extract. Especially
'a string array.
Dim PD1 As PROCESSOR_DATA
Dim PD2 As PROCESSOR_DATA

'These variables will be filled with the
'thread ID, by the CreateThread function.
Dim hThreadID1 As Long
Dim hThreadID2 As Long

'These variables will be filled with the handle
'of the thread, by the CreateThread function.
Dim hThread1 As Long
Dim hThread2 As Long

'These are global variables to tell the QTimer
'whether a thread is running or has completed.
DefByte RunSub1Chk = False
DefByte RunSub2Chk = False

'This is a global variable that the QTimer will use
'to tell "how many" threads it should watch for.
DefByte NumberOfThreads = 0

'This is a global variable that the thread(s) will
'fill with data (just to show they can be accessed).
Dim A(99,99) As Long

Dim Ticker As QTimer
    Ticker.Enabled = False
    Ticker.Interval = 60
    Ticker.OnTimer = ThreadCheck

Create MainForm As QForm
  Caption = " Single/Dual Thread(s)"
  DelBorderIcons(biMaximize)
  Width = 320
  Height = 240
  Left = (Screen.Width\2)-(MainForm.Width\2)
  Top = (Screen.Height\2)-(MainForm.Height\2)
  OnClose = MainForm_OnClose
  Create MainMenu As QMainMenu
    Create Thread1 As QMenuItem
      Caption = "Start Single"
      OnClick = Start_OnClick
    End Create
    Create Line1 As QMenuItem
      Caption = " | "
      Enabled = False
    End Create
    Create Thread2 As QMenuItem
      Caption = "Start Dual"
      OnClick = Start_OnClick
    End Create
    Create Line2 As QMenuItem
      Caption = " | "
      Enabled = False
    End Create
    Create Results As QMenuItem
      Caption = "Results"
      OnClick = Results_OnClick
      Enabled = False
    End Create
  End Create
  Create RichEdit as QRichEdit
    Align = alClient
    ReadOnly = True
    Font.Name = "Courier New"
    Font.Size = 10
    ScrollBars = ssBoth
    HideSelection = True
    WordWrap = False
  End Create
End Create

SetWindowLong(MainForm.Handle, GWL_HWNDPARENT, HWND_DESKTOP)
SetWindowLong(Application.Handle, GWL_HWNDPARENT, MainForm.Handle)

MainForm.ShowModal

Sub MainForm_OnShow
End Sub

Sub MainForm_OnClose
    'Make sure the QTimer is disabled.
    If Ticker.Enabled = True Then Ticker.Enabled = False
    'Make sure ALL threads that you started are
    'terminated before you terminate the application.
    If hThread1 Then
      TerminateThread(hThread1, 0)
      CloseHandle(hThread1)
      hThread1 = 0
    End If
    If hThread2 Then
      TerminateThread(hThread2, 0)
      CloseHandle(hThread2)
      hThread2 = 0
    End If
    Application.Terminate
End Sub

Sub Start_OnClick(Sender As QMenuItem)
    'Make sure any threads you started are
    'terminated before you start new ones
    'with the "same" thread handles.
    'You can start new threads as long as
    'their handle is not the same as one
    'already running.
    If hThread1 Then
      TerminateThread(hThread1, 0)
      CloseHandle(hThread1)
      hThread1 = 0
    End If
    If hThread2 Then
      TerminateThread(hThread2, 0)
      CloseHandle(hThread2)
      hThread2 = 0
    End If
    'Pretty neat way to avoid having
    'the end-user click on a menu option
    'that'll cause problems. Don't worry.
    'They'll be restored when the thread(s) are done.
    'I would NOT use this method until you
    'are 100% sure that it won't (in itself)
    'cause a problem!
    DisableMenus
    Select Case Sender.Handle
      Case Thread2.Handle
        'Just to inform the end-user that something is happening.
        RichEdit.AddString(" ")
        RichEdit.AddString("This may take a few moments.")
        RichEdit.AddString(" ")
        RichEdit.AddString("Starting dual threads.")
        'Needed to update the RichEdit properly.
        MainForm.Repaint
        'Set up some global variables that "each" thread will use.
        '(simply to verify that "each thread" is working correctly)
        PD1.Initial = 0  : PD1.Final = 49
        PD2.Initial = 50 : PD2.Final = 99
        'Set up a global variable that the QTimer will
        'use to determine how many threads to watch for.
        NumberOfThreads = 2
        'Start the first thread.
        hThread1 = CreateThread(0, 0, CodePtr(RunSub1), PD1, 0, VarPtr(hThreadID1))
        'Just to inform the end-user that the first thread has been started.
        RichEdit.AddString("Thread #1 started.")
        SetThreadPriority(hThread1, THREAD_BASE_PRIORITY_MIN)
        'Start the second thread.
        hThread2 = CreateThread(0, 0, CodePtr(RunSub2), PD2, 0, VarPtr(hThreadID2))
        'Just to inform the end-user that the second thread has been started.
        RichEdit.AddString("Thread #2 started.")
        SetThreadPriority(hThread2, THREAD_BASE_PRIORITY_MIN)
        'Start a QTimer to check for when the dual threads have completed.
        'Note: Don't use GetExitCodeThread to determine if
        '      a thread has completed. Because on NT systems...
        '      "The handle must have THREAD_QUERY_INFORMATION access."
        'So to avoid having to write extra code to determine the
        'end-users OS... just signal the end of any thread with
        'a "global" variable that the QTimer can check.
        Ticker.Enabled = True
      Case Thread1.Handle
        'Just to inform the end-user that something is happening.
        RichEdit.AddString(" ")
        RichEdit.AddString("This may take a few moments.")
        RichEdit.AddString(" ")
        RichEdit.AddString("Starting single thread.")
        'Needed to update the RichEdit properly.
        MainForm.Repaint
        'Set up some global variables that the thread will use.
        '(simply to verify that the thread is working correctly)
        PD1.Initial = 0 : PD1.Final = 99
        'Set up a global variable that the QTimer will
        'use to determine how many threads to watch for.
        NumberOfThreads = 1
        'Start the thread.
        hThread1 = CreateThread(0, 0, CodePtr(RunSub1), PD1, 0, VarPtr(hThreadID1))
        'Just to inform the end-user that the thread has been started.
        RichEdit.AddString("Thread #1 started.")
        SetThreadPriority(hThread1, THREAD_BASE_PRIORITY_MIN)
        'Start a QTimer to check for when the single thread has completed.
        'Note: Don't use GetExitCodeThread to determine if
        '      a thread has completed. Because on NT systems...
        '      "The handle must have THREAD_QUERY_INFORMATION access."
        'So to avoid having to write extra code to determine the
        'end-users OS... just signal the end of a thread with
        'a "global" variable that the QTimer can check.
        Ticker.Enabled = True
    End Select
    'When the thread(s) are done... the menu will be
    'available again. You can verify that the thread(s)
    'were processed and that they were able to access
    'globally defined variables with the "Results" menu option.
End Sub

Sub Results_OnClick
    'Disable the menus so the end-user doesn't
    'try to run any thread(s) again while the
    'data in the array is being displayed.
    DisableMenus
    'Display the data in the array.
    RichEdit.AddString(" ")
    DefInt ix, iy
    For ix = 0 To 99
      For iy = 0 To 99
        RichEdit.AddString(Format$("%.2d", ix)&" : "&Format$("%.2d", iy)&" = "&Format$("%.5d", A(ix,iy)))
        DoEvents
      Next iy
    Next ix
    'Restore the menus.
    EnableMenus
End Sub

Sub RunSub1(lpToPD1 As Long)
    'Set the global variable to tell QTimer that this thread is running.
    RunSub1Chk = True
    'CreateThread is passing a "pointer" to a variable list (UDT in our case).
    'So extract each variable from the list with MemCpy.
    DefLng Alpha = 0
    MemCpy(VarPtr(Alpha), lpToPD1, 4)
    DefLng Omega = 0
    MemCpy(VarPtr(Omega), lpToPD1+4, 4)
    'Start the elapse timer (for demo purposes).
    DefDbl t1 = Timer
    'Define the local variables.
    DefInt i, j, n
    'Repeat the following set of For/Next loops
    '25x, just so there's a noticeable elapsed time.
    For n = 1 To 50
      'Fill the portion of the array
      'based on the parameters passed.
      For i = Alpha To Omega
        For j = 0 To 99
          A(i,j) = j * i + j
          DoEvents
        Next j
      Next i
    Next n
    'End the elapse timer.
    t1 = Timer - t1
    'Just to inform the end-user of the elapsed time.
    RichEdit.AddString("Thread #1 Elapsed Time: " & Format$("%.3f",t1))
    'Because GetExitCodeThread is not being used
    '(to tell the caller that the thread has ended)...
    'tell the QTimer that it's ended with the global variable.
    RunSub1Chk = False
    'Don't put any code after the following line!
    'It'll never execute. ExitThread is essentially the same thing
    'as "Exit Sub" except that it sends an exit code to the caller.
    ExitThread(0)
End Sub

Sub RunSub2(lpToPD2 As Long)
    'Set the global variable to tell QTimer that this thread is running.
    RunSub2Chk = True
    'CreateThread is passing a "pointer" to a variable list (UDT in our case).
    'So extract each variable from the list with MemCpy.
    DefLng Alpha = 0
    MemCpy(VarPtr(Alpha), lpToPD2, 4)
    DefLng Omega = 0
    MemCpy(VarPtr(Omega), lpToPD2+4, 4)
    'Start the elapse timer (for demo purposes).
    DefDbl t2 = Timer
    'Define the local variables.
    DefInt i, j, n
    'Repeat the following set of For/Next loops
    '25x, just so there's a noticeable elapsed time.
    For n = 1 To 50
      'Fill the portion of the array
      'based on the parameters passed.
      For i = Alpha To Omega
        For j = 0 To 99
          A(i,j) = j * i + j
          DoEvents
        Next j
      Next i
    Next n
    'End the elapse timer.
    t2 = Timer - t2
    'Just to inform the end-user of the elapsed time.
    RichEdit.AddString("Thread #2 Elapsed Time: " & Format$("%.3f",t2))
    'Because GetExitCodeThread is not being used
    '(to tell the caller that the thread has ended)...
    'tell the QTimer that it's ended with the global variable.
    RunSub2Chk = False
    'Don't put any code after the following line!
    'It'll never execute. ExitThread is essentially the same thing
    'as "Exit Sub" except that it sends an exit code to the caller.
    ExitThread(0)
End Sub

Sub ThreadCheck
    DoEvents
    Select Case NumberOfThreads
      Case 2
        If RunSub1Chk = False And RunSub2Chk = False Then
          'Make sure the QTimer is disabled!
          Ticker.Enabled = False
          'Just to inform the end-user that "both" threads have been finished.
          RichEdit.AddString("Dual threads have finished.")
          'Make sure you restore the menu!
          EnableMenus
          'The array should now contain data.
          'So enable the "Results" menu option.
          Results.Enabled = True
          'I'm not sure if these are necessarily needed here.
          'But I added them anyways.
          TerminateThread(hThread1, 0)
          CloseHandle(hThread1)
          hThread1 = 0
          TerminateThread(hThread2, 0)
          CloseHandle(hThread2)
          hThread2 = 0
        End If
      Case 1
        If RunSub1Chk = False Then
          'Make sure the QTimer is disabled!
          Ticker.Enabled = False
          'Just to inform the end-user that the thread has been finished.
          RichEdit.AddString("Single thread has finished.")
          'Make sure you restore the menu!
          EnableMenus
          'The array should now contain data.
          'So enable the "Results" menu option.
          Results.Enabled = True
          'I'm not sure if this is necessarily needed here.
          'But I added it anyways.
          TerminateThread(hThread1, 0)
          CloseHandle(hThread1)
          hThread1 = 0
        End If
    End Select
End Sub

Sub DisableMenus
    Line2.Caption = "Please Wait..."
    Thread1.Visible = False
    Line1.Visible = False
    Thread2.Visible = False
    Results.Visible = False
End Sub

Sub EnableMenus
    Line2.Caption = " | "
    Thread1.Visible = True
    Line1.Visible = True
    Thread2.Visible = True
    Results.Visible = True
End Sub
