As with most Windows-based languages, GFA Basic can multithread (or multitask as it is sometimes referred to); however, apart from ReStop, there are no dedicated commands or functions for multithreading within GFA Basic 32 so Windows APIs are required to make it happen.
The description and examples below are not designed to be comprehensive - that would require a help file all to itself - but simply to give a basic framework on which it is possible to create a project with multithreading capability.
One note of caution before entering the world of multithreading: the smallest error inside auxiliary threads can cause fatal errors (or just very odd results) within GFABasic, as can failing to terminate threads properly and these are not always caught by the IDE and can lead to a forced closure of the program. Therefore, when working in this area, it is strongly advised to get into the habit of saving before running, even after the most minor of changes (this is advisable anyway, but more so with multithreading).
In addition, pressing Ctrl-Break while a multithreading program is running in the IDE can cause a fatal error which, once again, forces the interpreter to close. It is advisable to either let the program run its course or set up an alternative key combination in the program itself which is caught in Screen_KeyPreview and which will terminate all the auxiliary threads before ending the program.
The above two problems can, however, be mitigated by using Try/Catch structures as described in Help with Debugging below.
Finally, a brief word on some of the terminology used on this page and why it is important.
It is easy to get confused between a thread and a process. Simply put, a process is an occurence of a program which contains one or more threads. Hence, if you create new threads, they will all be contained inside the process which is your program.
But that does not fully describe what a thread actually is. To put it simply once more, a thread is a sequence of programmed instructions which are performed one after another by a CPU core in the order described by the instructions themselves. One or more threads can be contained within a process to do one or more instances of sequential processing of instructions at the same time, dependent upon how many cores are available to process the threads.
Whereas processes all have unique data sets, address tables, pointers, etc from other processes which are other occurences of the same program (e.g. if you have multiple instances of the GFA IDE open, they may stem from the same executable file but they are all separate processes which can run independently of each other and share nothing other than a few registry keys), this is not always the case with threads.
In general, auxiliary threads share all globally declared variables with the main thread or program as well as all globally declared objects and file handles, the Printer object and the address space; on the other hand, local variables, the Me object, the _DATA pointer, the psuedo registry values (Eax, etc), the Timer, root values for Rand and Rnd, the internal values for Crypt and the ERR error object (and others which are sadly not documented) are thread specific, as is the output device or window set within the thread itself by OUTPUT.
Finally, different threads can each share the same procedures and functions within the program listing, although it is strongly advised to keep the main procedure for the main thread only.
In the right program environment, multithreading can lead to vast improvements in speed and performance; however, in others, the effort to implement it into the program structure may outwiegh any potential benefits that may be gained.
For example, you are writing a program to render a 3-D scene with light emanating from the right and shadows cast by objects in the scene: the process of calculating ray-casting for the light and shadow production can be split into individual threads as the direction of the light and the size and position of the objects are predefined and so can each be calculated independently from the other. On the other hand, the actual rendering of the image would be harder to effectively split into threads as the image needs to be drawn from the back forwards so that objects that are nearer to the viewer are drawn over those that are further away. It would be possible to split the rendering process into a number of different threads but the effort involved may not merit the increase in performance.
Another example where multithreading would improve performance would be in the actioning of tasks at specific time intervals in the background, while the main program carries on with other business. See Creating a Timer Thread below for an example of this.
Finally, before using multithreading, the structure of GFABasic32 (it was never optimised for multithreading) and computers themselves should be considered.
For example, if you wish to render lines or other geometric shapes using the in-built GFA commands (e.g. Line, Box, etc), then multithreading is probably not the best option as many of these commands appear to use the same instance of the GDI library meaning that one command needs to be completed before the next can be processed; if, however, each thread creates its own instance of the GDI (or GDI+) library directly and then uses GDI library APIs to render the geometric displays, some speed improvements could well be noted.
A similar problem will also be experienced - but for different reasons - when using I/O channels. While it is possible to have numerous files open on different virtual and physical drives and partitions, there is only one physical connection between the motherboard the physical drive itself which will cause a bottleneck and reduce or even nullify any advantages gained by using multithreading.
While it is possible to have hundreds of threads active at the same time, it is best advice to have no more performing actions at any one time than you have cores in your CPU(s) (unless you have HyperThreading capability on an Intel chip or BullDozer on an AMD CPU, in which case no more than double the number). This is primarily due to the memory and register swapping that is performed by Windows to run more than one thread through a single core. The best analogy for this is shown when moving files from one drive to another. Say you have three drives: C, D, E. You wish to move one file from C to D and other from D to E and in an effort to save time you decide to move them both at the same time. If these files are large enough, what you will see is a slowing in the transfer rate of both files as drive D stops transferring data to E while it accepts bytes from C, then stops receiving from C to send more data to E. In the end, it is usually quicker to perform one file transfer, wait for it to finish, then do the second. The same stands with threads - it is generally quicker to perform one procedure after another than to multithread them both at the same time through the same core.
While you will be aware of the number of cores on your own computer, if you are planning to run your application on other systems, it could be an idea to ascertain how many cores it has before unleashing scores of threads on what could be a solitary CPU core. One way to do this is to use a variation of the following code:
Const wbemFlagReturnImmediately = &h10
Const wbemFlagForwardOnly = &h20
Local strComputer As String = "."
Local objwmiservice As Object, colitems As Object, objitem As Variant
Set objwmiservice = GetObject("winmgmts:\\" & strComputer & "\root\CIMV2")
Set colitems = objwmiservice.ExecQuery("SELECT * FROM Win32_Processor", "WQL", wbemFlagReturnImmediately + wbemFlagForwardOnly)
For Each objitem In colitems
Print "NumberOfCores: "; objitem.NumberOfCores
Print "NumberOfLogicalProcessors: "; objitem.NumberOfLogicalProcessors
Next
NumberofCores returns the number of physical cores, NumberofLogicalProcessors the theoretical number of available cores (i.e. if Hyperthreading is Enabled, the latter will be twice the former). You can get good results running as many threads as you have logical processors, but keep in mind that, if they exceed the number of physical cores, you are still running multiple threads through a single core - albeit one engineered to perform this task better than a standard CPU core.
While the above example can be expanded to return a wealth of information, it can cause a program to pause while it retrieves it. A quicker but less informative method of getting the number of virtual (not physical) cores is by using the GetSystemInfo() API as shown below:
// Courtesy of Troy Cheek
Type SYSTEM_INFO
wProcessorArchitecture As Card
wReserved As Card
dwPageSize As Long
lpMinimumApplicationAdress As Long
lpMaximumApplicationAdress As Long
dwActiveProcessorMask As Long
dwNumberOfProcessors As Long
dwProcessorType As Long
dwAllocationGranularity As Long
wProcessorLevel As Card
wProcessorRevision As Card
End Type
Local si As SYSTEM_INFO
~GetSystemInfo(V:si)
Print "Number of Virtual Cores:"; si.dwNumberOfProcessors
With all that said, with the steady progress of computer technology and operating systems, modern computers are becoming more and more proficient in handling multithreading. To see how your computer handles it, run the example in Passing Values using Parameters below.
The API used to create a thread is:
ThreadHandle = CreateThread(ThreadAttr, StackSize, ProcAddr, Parameter, CreationFlags, OUT ThreadId)
A full description of this API can be found here; for the purposes of this tutorial, the pertinent values and parameters are:
As to closing threads, there is much debate about which API(s) to use. In C++, for example, the moment the function called by the thread has finished, the thread is closed and all attached values and address blocks tidied up automatically; therefore, you may be told by many people that no API is required to properly exit a thread. According to the original GFA development team, this is not the case with GFABasic and they advice that the following API be placed in the last executed line of any thread:
~ExitThread(ExitCode)
This API does not have a return value (hence the ~ or VOID which should precede it) and ExitCode is an arbitary value which tells any other thread querying the status of the thread with GetExitCodeThread(ThreadHandle, OUT ExitCode) that is has closed.
Another API which causes much debate is:
retval = TerminateThread(ThreadHandle,ExitCode)
TerminateThread() is used remotely to immediately close a thread (supplying an arbitrary ExitCode as before); if the action is successful, retval will be non-zero; if there was an error, retval will be zero and the cause of the error can be retrieved by using GetLastError(). However, this API should be used with caution as it may not be possible to gauge the status and actions of the thread - for example, it may be saving data to a file - when the command is issued. In addition, the GFA developers strongly advice against its use and, prior to Windows Vista, it was susceptible to memory leaks and may not always clear the address tables and values related to the thread. For Vista onwards, see here for more information. Hence, this should only be used if the thread did not exit when it should, or if a fatal error occured within the main program, and the thread needs to be forcibly terminated.
The following example shows how to create and terminate a simple thread.
OpenW 1 : Win_1.AutoRedraw
Try
Global Int32 n = 0, threadHandle, threadID : Global Large n1
threadHandle = CreateThread(0, 0, ProcAddr(ThreadTwo), 100000, 0, V:threadID)
For n = 1 To 10 : Print AT(1, 1); "Main Thread output: "; n , "Thread two output:"; n1 : Delay 1 : DoEvents : Next n
Catch
threadID = 0
~TerminateThread(threadHandle, 0)
Message SysErr(GetLastError())
EndCatch
CloseW 1
Procedure ThreadTwo(startval%)
n1 = startval%
While threadID <> 0 And n < 11 : Inc n1 : Wend
~ExitThread(0)
EndProcedure
The above example creates a thread to increment a value by one from a given start value as fast as possible, while the main part of the program prints its progress every second and then terminates the auxiliary thread and then itself once ten one second intervals have elapsed.
Please note that the main thread will forcibly attempt to closeThreadTwo if an error occurs in the program; additionally, if this fails, the second thread (ThreadTwo) has an internal check to stop the loop when the time count 'n' passes 10 or if the main program sets ThreadId to 0 in the event of such an error. This will not cover all eventualities but it shows some of the measures that can be used to limit the damage caused by an error in the programming.
As explained above, the CreateThread() API's fourth parameter allows you to pass a value to the procedure which is to be started in the new thread. The parameter is, in fact, a pointer, so can represent any non-variant variable type even if that variable type does not match that declared in the procedure, as is shown in the example below. However, it should be noted that, on occasions, the way that GFABasic32 passes Double values can cause errors if the variable in the procedure is not specifically declared, especially if it is being used as a pointer to an array (see below for more information on passing arrays).
Local a% = 7, b# = 3.77, th%(3), tid%(3)
OpenW 1, 10, 10, 200, 400 : Win_1.AutoRedraw = 1
Try
th%(1) = CreateThread(0, 0, ProcAddr(ThreadTwo), a%, 0, V:tid%(1))
th%(2) = CreateThread(0, 0, ProcAddr(ThreadTwo), b#, 0, V:tid%(2))
th%(3) = CreateThread(0, 0, ProcAddr(ThreadTwo), 1.222, 0, V:tid%(2))
Catch
~TerminateThread(th%(1), 0)
~TerminateThread(th%(2), 0)
~TerminateThread(th%(3), 0)
EndCatch
Procedure ThreadTwo(value%)
Output = Win_1 : Text 10, (value% * 15), value%
~ExitThread(0)
EndProcedure
Each of the above values will be passed, including the numeric constant, although only a rounded integer will be received by the procedure. Another point to note from this example is that the same procedure can be used for separate threads without causing a conflict.
Passing strings is a little more complicated: setting the parameter in the receiving procedure (the one that will be part of the new thread) to a string as in Procedure ThreadTwo(value$) and setting the parameter in the CreateThread() function to either value$ or v:value$ will cause a fatal error which will not be caught by the Try/Catch loop.
The only way to send a string parameter is by setting the parameter as the variable pointer of the string and then reconstructing the string within the procedure itself. Also note that the string MUST be a global, not a local, variable as the address area of global variables are shared between threads, whereas those of local variables are not. This method is best shown by the following example:
Global a$ = "Hello"
Local th%(3), tid%(3)
OpenW 1, 10, 10, 200, 400 : Win_1.AutoRedraw = 1
Try
th%(1) = CreateThread(0, 0, ProcAddr(ThreadTwo), V:a$, 0, V:tid%(1))
Catch
~TerminateThread(th%(1), 0)
EndCatch
Procedure ThreadTwo(v%)
Local slen = {v% - 4}, value$ = Space(slen)
BlockMove v%, V:value$, slen
Output = Win_1 : Text 10, 10, value$
~ExitThread(0)
EndProcedure
At the time of writing this article, no method has yet been found for passing a Variant.
It is, however, possible to pass numerical and fixed string arrays by using a similar method as that used for the string (with an extra element added which contains the upper limit of the array - see example below for an example), but it is simpler to copy a global array into a local once the thread has begun.
Local n As Int32
Global a%() : ReDim a%(10)
For n = 0 To 10 : a%(n) = n : Next n
ReDim a%(UBound(a%()) + 1) : Insert a%(0) = UBound(a%()) - 1
PassArrays(V:a%(0))
Delete a%(0) : ReDim a%(UBound(a%()) - 1)
For n = 0 To 10 : Print AT(20, n + 1); "a%(" & Trim(n) & ") =" & a%(n) : Next n
Procedure PassArrays(vp%)
Local ub% : BlockMove vp%, V:ub%, 4 : Add vp%, 4
Local b%(ub%), n%
BlockMove vp%, V:b%(0), ((ub% + 1) * 4)
For n = 0 To ub% : Print AT(1, n + 1); "b%(" & Trim(n) & ") =" & b%(n) : Next n
EndProcedure
However, what is the point of sending a value to the thread when that thread can read a global variable direct to get the same information? The answer is: when each thread requires an individual value to differentiate itself from the others created by the same procedure. It is true that, once again, you could change the value of global variable prior to the creation of each thread to do the same job, but using the supplied parameter is much cleaner, more convenient, quicker and makes for tidier code; also, due to other system activity, the relevant change in the global variable can not always be guaranteed to coincide with the creation and start of operation of the thread (there are methods to achieve this - see Restricting and Controlling Thread Access below - but, once again, just using the supplied parameter is much easier).
The following example shows this point: it passes an individual value to each thread so that that thread knows which element of a%() it is set to increment and the unique number then aids the program to identify in what order the threads finished.
This example gives you a good tool for showing how well (or not) your computer performs with multithreading - the results are even better if you compile it into an executable. Also note the warning about screen freeze: the CPU requires use of at least one core to render the results on the screen; if all the cores are being used to their full potential, the rendering does not get done after the first few scans until the threads start exiting and freeing up processor time.
Finally, this example illustrates that the threads do not perform at the same speed or finish in the same order that they were created; in fact, repeated runs will show there is no pattern to performance.
OpenW 1 : Win_1.AutoRedraw = 1
Local threads% = Val(InputBox("Number of Threads (2-20)")), t# = Timer, t1#, t2#, y% = 2
If threads% > 20 Then threads% = 20 Else If threads% < 2 Then threads% = 2
Global a%(threads%), p%, pst%(threads%), tct%, tmax% = 100000000
Print AT(1, 1); "The display may freeze if you are using more threads than your computer has cores."
MainThread(threads%, y%)
y% = y% + threads% + 3
t1# = Timer - t#
Print AT(1, y% - 1); "Time taken with" & threads% & " threads:" & t1#
tmax% = tmax% * threads% : p% = 0 : t# = Timer : a%(1) = 0
MainThread(1, y%)
t2# = Timer - t#
Print AT(1, y% + 3); "Time taken with 1 thread:" & t2#
Print AT(1, y% + 5); "Multi-threading was " & Format(1 - (t1# / t2#), "00%") & " quicker"
Print AT(1, y% + 7); "You may now close the window"
Do : Sleep : Until Win_1 Is Nothing
Procedure MainThread(thr%, y%)
Local n%, th%(thr%), tid%(thr%)
Try
tct% = thr%
For n% = 1 To thr%
th%(n%) = CreateThread(0, 0, ProcAddr(ThreadTwo), n%, 0, V:tid%(n%))
Next n%
While tct% > 0
For n% = 1 To thr%
Print AT(1, n% + y%); "Output from thread" & n% & ":" & a%(n%)
Next n%
DoEvents
If tct% > 0 Then Delay .1
Wend
For n% = 1 To thr%
Print AT(1, n% + y%); "Output from thread" & n% & ":" & a%(n%) & Position(pst%(n%))
Next n%
Catch
Message "Error Caught"
EndCatch
EndProcedure
Function Position(p%)
If p% = -1 Then Return " Closed on Error"
Return " Position:" & p% & Iif(p% = 1, "st", Iif(p% = 2, "nd", Iif(p% = 3, "rd", "th")))
EndFunction
Procedure ThreadTwo(ct%)
Try
While a%(ct%) < tmax% : Inc a%(ct%) : Wend
Dec tct% : Inc p% : pst%(ct%) = p%
~ExitThread(0)
Catch
Dec tct% : pst%(ct%) = -1
~ExitThread(0)
EndCatch
EndProcedure
It is possible to create a thread when you need it, then close it, then open it when you need it again...and so on. This is strongly advised against, especially using GFABasic 32, as continually creating and destroying threads increases the chances of an error (for many, many reasons).
An alternative is to create one or more global flag variables which notify the thread when it should be active and when not. When the thread is no longer required, either the main thread, another thread or the affected thread itself can send a message to suspend its operation using the API SuspendThread(ThreadHandle); when the thread is required again either the main thread or another auxiliary thread can then restart the thread using the ResumeThread(ThreadHandle) API.
Instead of SuspendThread() and ResumeThread(), it is possible to use While not resume? : Wend or similar methods to suspend operations within the thread and this works equally as well as using the APIs as long as the number of active threads is less than the number of available physical cores. Once the number of active threads exceeds that number, using the API method becomes more efficient as, while suspended, the thread does nothing and requires no CPU time; the While/Wend loop, while doing nothing programmatically, is constantly querying values and thus keeps the thread active when it should be inactive; you can enter Delay n or Sleep n commands to reduce the impact on the CPU but this only lessens rather than stills the thread activity and any resumption of activity has to wait for the specified time interval to end.
Before showing SuspendThread() and ResumeThread() in action, it may be useful to note that it is possible to create a thread which is already in suspended mode by setting the CreationFlags (the fifth) parameter to $4. This can be useful if you want to create all threads that a program will require at some time, but not necssarily straight away, in one go.
The example below runs a number of counters, determined by the value set in threads%, in different threads to different maximums, pauses them until all the threads have finished, then resumes the count so that all threads end on the same figure, at which stage the threads are terminated. The first run uses a While:Wend loop to pause operation, while the second uses the API method. Change the number of threads to see how performance differs between the two methods depending on whether or not the number of threads exceeds the number of physical and/or logical cores on your system. In addition, note that the second run creates all the threads in suspended mode, resuming them only once all threads exist.
Global threads% = 12
Global ct(threads%) As Int32, flag As Byte = threads% * 2, stat(threads%) As Byte, swit?
Local Int32 n, th(threads%), tid(threads%) : Local t# = Timer
Const CREATE_SUSPENDED = $4
OpenW 1
Try
For n = 1 To threads% : th(n) = CreateThread(0, 0, ProcAddr(OtherThreads), n, 0, V:tid(n)) : Next n
While flag <> 0 : Display(0) : Wend
Catch
For n = 1 To threads% : ~TerminateThread(th(n), 0) : Next n
EndCatch
Display(0)
Print AT(1, threads% + 2); "Time taken:"; Timer - t#
swit? = True : t# = Timer : flag = threads% * 2 : ArrayFill ct(), 0 : ArrayFill stat() , 0
Try
// Create threads suspended until all threads are created
For n = 1 To threads% : th(n) = CreateThread(0, 0, ProcAddr(OtherThreads), n, CREATE_SUSPENDED, V:tid(n)) : Next n
For n = 1 To threads% : ~ResumeThread(th(n)) : Next n
While flag <> 0
Display(threads% + 3)
If flag = threads% Then For n = 1 To threads% : ~ResumeThread(th(n)) : Next n
Wend
Catch
For n = 1 To threads% : ~TerminateThread(th(n), 0) : Next n
EndCatch
Display(threads% + 3)
Print AT(1, (threads% * 2) + 5); "Time taken:"; Timer - t#
Do : Sleep : Until Win_1 Is Nothing
Procedure Display(y As Int32)
Local Int32 n
For n = 1 To threads%
Print AT(1, n + y); "Output of Aux Thread"; n; ":"; ct(n) & Choose(stat(n) + 1, Space(20), " Paused", " Ended ")
Delay .01
Next n
EndProcedure
Procedure OtherThreads(tn As Int32)
Local Int32 limit = tn * 10000000
While ct(tn) < limit : Inc ct(tn) : Wend
stat(tn) = 1 : Dec flag
If Not swit?
// First time, wait using While/Wend
While flag > threads% : Wend
Else
// Second time, wait by suspending the thread
~SuspendThread(GetCurrentThread())
EndIf
stat(tn) = 0
limit = limit + (((threads% + 1) - tn) * 10000000)
While ct(tn) < limit : Inc ct(tn) : Wend
stat(tn) = 2 : Dec flag
~ExitThread(0)
EndProcedure
For another method of suspending and resuming threads, see the next section.
As with processes and windows, it is possible to pass messages between threads. The usefulness of this facility is reduced by the fact that all threads can read global variables, so rather than constantly using the PeekMessage() API to check a message queue, it is easier to use While nomessage? : (* Perform Task *) : Wend where nomessage? is a global boolean flag which is set or reset by another thread.
However, messages can come into their own when the thread is waiting for a new task to perform and where a While:Wend loop would hog CPU resources unneccesarily. As shown in the section above, it is possible to suspend the thread while it is inactive and then resume it when it is needed again; alternatively you could use GetMessage(Message, SendingHandle, MinFilter, MaxFilter) within the thread itself (the only parameter you are likely to use with threads is the first one which is a pointer to the message content data structure). The advantage of GetMessage() over PeekMessage() is that it waits for a message before continuing so, even within a While/Wend loop, it acts very much like SuspendThread() in reducing the demand on the CPU to an absolute minimum. Note: Whenever you use GetMessage() it does not actually action the message once it has been received, so you should follow it with the DispatchMessage(Message) API to do just that.
To send a message to a thread, the PostThreadMessage(ThreadID, MessageType, wParam, lParam) API can be used, where MessageType is a WM_xxx windows message type (usually WM_APP + 1 to send non-system messages) and wParam and lParam can contain additional message information. The message is then received by the thread in a Windows message data structure (the data type is given in the example below).
The following example is almost identical to the one in the section above, except that the first run is done using the PostThreadMessage() and GetMessage APIs to pause and resume work in the threads. It is interesting that processing times are almost identical; the main difference is that, when threads exceed cores, Win_1 is rendered more smoothly and regularly using messages than by suspending the threads.
Type MSG
hWnd As Handle (* Always Null with threads *)
- Int32 message, wParam, lParam, time (* time is the system time when the message was sent *)
pt As POINT (* The position of the cursor when the message was sent *)
EndType
Type POINT
- Int32 x, y
EndType
Global threads% = 16
Global ct(threads%) As Int32, flag As Byte = threads% * 2, stat(threads%) As Byte, swit?
Local Int32 n, th(threads%), tid(threads%) : Local t# = Timer
Const CREATE_SUSPENDED = $4
OpenW 1
Try
For n = 1 To threads% : th(n) = CreateThread(0, 0, ProcAddr(OtherThreads), n, 0, V:tid(n)) : Next n
While flag <> 0
Display(0)
If flag = threads% Then For n = 1 To threads% : ~PostThreadMessage(tid(n), WM_APP + 1, 1, 0) : Next n
Wend
Catch
For n = 1 To threads% : ~TerminateThread(th(n), 0) : Next n
EndCatch
Display(0)
Print AT(1, threads% + 2); "Time taken:"; Timer - t#
swit? = True : t# = Timer : flag = threads% * 2 : ArrayFill ct(), 0 : ArrayFill stat() , 0
Try
// Create threads suspended until all threads are created
For n = 1 To threads% : th(n) = CreateThread(0, 0, ProcAddr(OtherThreads), n, CREATE_SUSPENDED, V:tid(n)) : Next n
For n = 1 To threads% : ~ResumeThread(th(n)) : Next n
While flag <> 0
Display(threads% + 3)
If flag = threads% Then For n = 1 To threads% : ~ResumeThread(th(n)) : Next n
Wend
Catch
For n = 1 To threads% : ~TerminateThread(th(n), 0) : Next n
EndCatch
Display(threads% + 3)
Print AT(1, (threads% * 2) + 5); "Time taken:"; Timer - t#
Do : Sleep : Until Win_1 Is Nothing
Procedure Display(y As Int32)
Local Int32 n
For n = 1 To threads%
Print AT(1, n + y); "Output of Aux Thread"; n; ":"; ct(n) & Choose(stat(n) + 1, Space(20), " Paused", " Ended ")
Delay .01
Next n
EndProcedure
Procedure OtherThreads(tn As Int32)
Local Int32 limit = tn * 10000000 : Local t2msg As MSG
While ct(tn) < limit : Inc ct(tn) : Wend
stat(tn) = 1 : Dec flag
If Not swit?
// First time, wait using GetMessage()
While GetMessage(t2msg, 0, 0, 0)
~DispatchMessage(t2msg)
If t2msg.message = WM_APP + 1 And t2msg.wParam = 1 Then Exit Do
Wend
Else
// Second time, wait by suspending the thread
~SuspendThread(GetCurrentThread())
EndIf
stat(tn) = 0
limit = limit + (((threads% + 1) - tn) * 10000000)
While ct(tn) < limit : Inc ct(tn) : Wend
stat(tn) = 2 : Dec flag
~ExitThread(0)
EndProcedure
Other APIs you may find useful when using messages with threads are PeekMessage(), GetQueueStatus() and WaitMessage(). For a full listing of message API functions, see here.
Although modern operating systems are perfectly capable of assessing thread workloads and assigning CPU/core time accordingly, there are rare occasions when it may be advantageous to determine this yourself. For example, if there are multiple threads working on a problem and some have light workloads and others heavier, rather than relying on the operating system to use its alogorithms to work this out once the threads are active, it may save precious microseconds to allocate individual cores to the threads with a heavy workload and bundle those with lighter workloads all onto a single core.
The API used to allocate threads to specific cores is SetThreadAffinityMask(ThreadHandle, AffinityMask) where AffinityMask is an integer of up to 64bits where the bit or bits set signify the core or cores that is/are to be assigned. For example, SetThreadAffinityMask(Thread2Handle, %100) allocates core 3 to thread two; if AffinityMask had been %10000, then core 5 would be allocated, or %11 then both cores 1 and 2 would be allocated. See here for more information.
The following is a simple example of how to use this API:
// Acknowledgements to TroyCheek
Global Int32 threadHandle, threadID, mainHandle
mainHandle = GetCurrentThread()
threadHandle = CreateThread(0, 0, ProcAddr(bmp2png), 0, 0, V:threadID)
~SetThreadAffinityMask(threadHandle, %10) // Thread 2 to core 2
~SetThreadAffinityMask(mainHandle, %1) // Main thread to core 1
To cancel the affinity, repeat the command with AffinityMask set to zero.
Note: The SetThreadAffinityMask should be used with caution as it can upset the operation of pre-assigned system thread/core operations.
One common reason for creating a separate thread is to perform an action once or more after a specific time interval. While it is possible to use the methods outlined in the sections above to control such a timed event, none of them could be guaranteed to call the timer thread at exactly the time interval required due to thread allocation and background tasks all competing for CPU time.
Therefore, to create a timer thread, it is advisable to use the Multimedia Timer functions instead.
The first two functions that you will need to know are the timeBeginPeriod(TimePeriod) and timeEndPeriod(TimerPeriod). These two functions must bracket all other Multimedia Timer functions used in a particluar session and the TimePeriod value, which sets the minimum application timer resolution in mlliseconds, must be the same in both functions. It may be tempting to set TimePeriod to 1, but unless this is truly necessary, this can cause all other threads and applications to slow markedly.
The next function that you will require is the one that sets up the call back routine itself: TimerEventId = timeSetEvent(Delay, Resolution, TimeProcAddr, Parameter, FreqEvent) where the parameters are:
The TimeProc function itself has a pre-defined array of parameters and takes the form TimeProc(TimerEventId, Msg, Parameter, Reserved1, Reserved2). TimerEventID is identical to the ID reference returned by timeSetEvent() while Parameter is the value passed by that function; the remaining parameters are all reserved for system use.
Finally, the last function you will need to know (there are others which can be found here) is timeKillEvent(TimerEventID) which should be used to end the Timer event from inside the event itself.
Below is a very short example which counts up to a certain number, printing every 10th cycle in Window 1, and uses the Timer event to calculate and show running time in a second window. Finally, an approximate time check is performed to test the accuracy of the Timer event. In most tests, it gets to within a few hundredths of a second. Note that all Time Event functions and constants need to be declared first.
// Acknowledgements to the original GFA Basic Development Team
Declare LIB "winmm"
Declare Function timeBeginPeriod (ByVal p%) As Long
Declare Function timeEndPeriod(ByVal p%) As Long
Declare Function timeKillEvent (ByVal id%) As Long
Declare Function timeSetEvent (ByVal uDelay%, ByVal uResolution%, ByVal lpTimeProc As Handle, ByVal user%, ByVal fEvent%) As Int
Enum TIME_ONESHOT, TIME_PERIODIC
OpenW 2, 210, 10, 200, 200
OpenW 1, 10, 10, 200, 200
Global EventId?, ThreadEnded?
Local ct As Int32, t# = Timer
timeBeginPeriod(10)
EventId = timeSetEvent(10, 5, ProcAddr(TimeProc), 0, TIME_PERIODIC)
While ct < 10000000 : Inc ct
If ct / 10 = Int(ct / 10) Then Text 10, 10, "Count:" & ct & " "
Wend
t# = Timer - t# : ThreadEnded? = True
Text 10, 30, "Time Check: " & Format(t#, "0.00")
timeEndPeriod(10)
Do : Sleep : Until IsNothing(Win_1) Or IsNothing(Win_2)
CloseW 1 : CloseW 2
Procedure TimeProc(uID%, Msg%, Param%, Res1%, Res2%)
Static tct As Large
Output = Win_2
Inc tct
Text 10, 10, Format(tct / 100, "0.00") & " seconds "
If ThreadEnded? Then timeKillEvent(uID%)
EndProcedure
Windows and GFA Basic are well designed for multithreading, with much of the data architecture being either unique to the individual threads or, where there is shared access, there are structures in place to control and restrict access to prevent errors. However, there are still some areas where there is potential for data conflict errors to occur, such as multiple threads reading from and writing to a random access data file or database or external data stream or, in the case of GFA Basic, having access to the Debug object and form.
When such a situation arises, there are three tools within Windows that can help you impose controlled access within your program structure: Critical Sections, Mutexs and Semaphores. All three are small objects which can span threads (and processes) and through which the granting of access is ultimately controlled by the operating system itself. Critical Sections and Mutexs restrict access to a resource to only one thread at a time, while Semaphores restrict the number of threads which are granted access to within a specified number range.
There are four APIs that you will need to use Critical Sections in your program: InitializeCriticalSection(CriticalSection) which creates the control object; EnterCriticalSection(CriticalSection) which requests permission to gain ownership of the control; LeaveCriticalSection(CriticalSection) which gives up ownership of the control; and DeleteCriticalSection(CriticalSection) which destroys the control. In addition, there are two further APIs which you may find useful: TryEnterCriticalSection(CriticalSection) and InitializeCriticalSectionAndSpinCount(CriticalSection, SpinCount) - see here for more information. In each of these, CriticalSection is a pointer to the Critical Section control which is initialised or created by InitialiseCriticalSection().
Below is a quick example of how to use a Critical Section to control output to the Debug object:
Type CRITICAL_SECTION
- Int32 Dummy(5)
EndType
Type MSG
hWnd As Handle
- Int32 message, wParam, lParam, time
pt As POINT
EndType
Type POINT
- Int32 x, y
EndType
Const CREATE_SUSPENDED = $4
'
Debug.Show
Local Int32 ct, n, threads = 10, th(threads), tid(threads)
Local mess As MSG
Global cs As CRITICAL_SECTION, mtid = GetCurrentThreadId()
// Initialise Critical Section
~InitializeCriticalSection(V:cs)
// Create the threads in suspended mode
For n = 1 To threads : th(n) = CreateThread(0, 0, ProcAddr(ProcessThreads), n, CREATE_SUSPENDED, V:tid(n)) : Next n
// Activate all the threads
For n = 1 To threads : ~ResumeThread(th(n)) : Next n
// Wait for all the threads to signal that they have finished
While GetMessage(mess, 0, 0, 0)
~DispatchMessage(mess)
If mess.message = WM_APP + 1 And mess.wParam = 2 Then Inc ct
If ct = threads Then Exit Do
Wend
// Delete the Critical Section
~DeleteCriticalSection(cs)
Procedure DoDebugPrint(txt$)
~EnterCriticalSection(cs)
Debug.Print txt$
~LeaveCriticalSection(cs)
EndProcedure
Procedure ProcessThreads(tn%)
Local Int32 n
For n = 1 To 10
DoDebugPrint("This is text from Auxiliary Thread" & tn%)
Delay .005 // Simulates other work
Next n
~PostThreadMessage(mtid, WM_APP + 1, 2, 0)
~ExitThread(0)
EndProcedure
Taking a look at the output shows that Windows does not allocate access to a Critical Section in any logical order, which could be a major drawback if you were writing data to a sequential data file, for example. This can be got around by inserting an additional customised condition on access through a global variable as in the example below where the integer order is used. This can have the disadvantage of being significantly slower, though.
Type CRITICAL_SECTION
- Int32 Dummy(5)
EndType
Const CREATE_SUSPENDED = $4
'
Debug.Show
Global cs As CRITICAL_SECTION, mtid As Int32 = GetCurrentThreadId(), order As Int32 = 1, threads As Int32 = 10
Local Int32 ct, n, th(threads), tid(threads)
// Initialise Critical Section
~InitializeCriticalSection(V:cs)
// Create the threads in suspended mode
For n = 1 To threads : th(n) = CreateThread(0, 0, ProcAddr(ProcessThreads), n, CREATE_SUSPENDED, V:tid(n)) : Next n
// Activate all the threads
For n = 1 To threads : ~ResumeThread(th(n)) : Next n
// Wait for all the threads to finish
~WaitForMultipleObjects(threads, V:th(1), True, 10000)
// Delete the Critical Section
~DeleteCriticalSection(cs)
Function DoDebugPrint(txt$, tn%) As Boolean
If tn% = order
~EnterCriticalSection(cs)
Debug.Print txt$
~LeaveCriticalSection(cs)
Inc order : If order = threads + 1 Then order = 1
DoDebugPrint = True
EndIf
EndFunction
Procedure ProcessThreads(tn%)
Local Int32 n = 1
While n < 11
If DoDebugPrint("This is text from Auxiliary Thread" & tn%, tn%) Then Inc n
Delay .005 // Simulates other work
Wend
~ExitThread(0)
EndProcedure
Note that the While GetMessage(Msg, 0, 0, 0) : Wend loop has been replaced by the WaitforMultipleObjects() API which performs exactly the same function. The syntax of this API is WaitforMultipleObjects(NoOfThreads, HandleArray, WaitForAll, TimeOut) where NoOfThreads is the number of threads to wait for, HandleArray is a pointer to an array which stores the handles of each thread to wait for, WaitForAll is a boolean value which determines whether the function waits for all threads to terminate and TimeOut is the time in milliseconds before the wait is aborted; the constant INFINITE can be used in the last parameter to force the function to wait until all threads are closed. For the return values, see the description of WaitforSingleObject() below.
Mutex stands for Mut[ually] Ex[clusive] and, true to their name, Mutex objects work on the same lines as Critical Sections: an object is created which, controlled by the operating system, allows access to only one thread at a time on a first come, first served basis, to one or more specified areas of your program.
To create a Mutex, you need to use the following API: MutexID = CreateMutex(SecAttrs, Ownership, Name). Of the parameters, MutexID is the ID of the Mutex created (or Null is the function failed) and Ownership is a boolean which specifies whether the thread which created the Mutex has initial ownership of it; the other two parameters are optional and usually unused - see here for more information on them.
Unlike Critical Sections, there is no specific API for requesting ownership of an unnamed Mutex; instead, the WaitforSingleObject(Handle, TimeOut) API is used instead, where the Handle is the MutexID and TimeOut is the number of milliseconds before the API gives up waiting if unsuccessful; the constant INFINITE can be used to force the function to wait indefinitely. The return value of this function can be WAIT_OBJECT_n which shows that the wait has been successful for object n (usually 0 for this function, but can vary for WaitforMultipleObjects()), WAIT_ABANDONED which is returned with Mutexs when the state of the Mutex can not be determined (with an added _n for WaitforMultipleObjects() to show which object caused this result), WAIT_TIMEOUT if the TimeOut time is exceeded or WAIT_FAILED if an error occured.
Once the thread which has ownership of the Mutex has no further use for it, the ReleaseMutex(MutexID) API is called. Oddly, once the Mutex is no longer required, there is no dedicated API which destroys it; instead the CloseHandle(MutexID) API should be used.
Below is a quick example showing how to use a Mutex to restrict access to the Debug object.
Const WAIT_OBJECT_0 = $0, WAIT_ABANDONED = $80, WAIT_TIMEOUT = $102, WAIT_FAILED = $FFFFFFFF
Const CREATE_SUSPENDED = $4
'
Debug.Show
Global Int32 mh, order = 1, threads = 10
Local Int32 ct, n, th(threads), tid(threads)
// Create Mutex
mh = CreateMutex(Null, False, Null)
// Create the threads in suspended mode
For n = 1 To threads : th(n) = CreateThread(0, 0, ProcAddr(ProcessThreads), n, CREATE_SUSPENDED, V:tid(n)) : Next n
// Activate all the threads
For n = 1 To threads : ~ResumeThread(th(n)) : Next n
// Wait for all the threads to finish
~WaitForMultipleObjects(threads, V:th(1), True, 10000)
// Delete the Critical Section
~CloseHandle(mh)
Function DoDebugPrint(txt$, tn%) As Boolean
Local Int32 wait
If tn% = order
wait = WaitForSingleObject(mh, INFINITE)
Select wait
Case WAIT_OBJECT_0
Debug.Print txt$
Inc order : If order = threads + 1 Then order = 1
DoDebugPrint = True
EndSelect
~ReleaseMutex(mh)
EndIf
EndFunction
Procedure ProcessThreads(tn%)
Local Int32 n = 1
While n < 11
If DoDebugPrint("This is text from Auxiliary Thread" & tn%, tn%) Then Inc n
Delay .005 // Simulates other work
Wend
~ExitThread(0)
EndProcedure
As described above, Semaphores are the one control that allow more than one thread to have access to a particular part of your program or incoming/outgoing data source. It has a similar syntax to Mutexs in that requesting ownership is done through WaitforSingleObject() and destroying the Semaphore control through CloseHandle().
To create a Semaphore use: SemaphoreID = CreateSemaphore(SecAttr, InitialCount, MaxCount, Name). SemaphoreID is the handle for the Semaphore created by the function (or Null if the function failed), InitialCount is the initial number of threads allowed access on the creation of the Semphore (> 0 and <= MaxCount) while MaxCount is the maximum amount of threads allowed access at any time. The other two parameters are not particularly relevant to this article and more information on them and this function can be found here.
Finally, to release a Semaphore, use: ReleaseSemaphore(SemaphoreID, Increment, OUT LastValue). When a Semaphore is released, the maximum number of possible semaphores is automatically reduced by one; Increment can be used to increase this total again, a value of 1 (one) keeping the maximum as it was, a value of greater than one increasing it beyond its previous level. LastValue is a returned integer value (enter Null if it is not required) which holds the last value of the maximum number of Semaphores before the last release. Finally, the function returns a zero if an error ocurred in execution.
Below is an example of how to use Semaphores to allow access, in this case to the procedure DisplayBox which displays all active threads in red and all inactive threads in black. Note that the DisplayBox procedure is controlled by a Mutex to ensure that only one box is rendered at a time; also note that, as Win_1 was opened in the main thread, any changes to the Win_1.Font (transparency, size, etc) will cause a non-fatal failure in this routine which will not release the Mutex and stop the display updating.
Const WAIT_OBJECT_0 = $0, WAIT_ABANDONED = $80, WAIT_TIMEOUT = $102, WAIT_FAILED = $FFFFFFFF
Const CREATE_SUSPENDED = $4
'
OpenW Full 1 : Win_1.AutoRedraw = 1
Win_1.FontSize = 10 : Win_1.FontTransparent = True : Win_1.FontBold = True
Local Int32 n, threads = 10, th(threads), tid(threads)
Global exitall?, mh As Int32, sm As Int32, t# = Timer, x As Int32
sm = CreateSemaphore(Null, Min(2, threads), Min(2, threads), Null)
mh = CreateMutex(Null, False, Null)
x = Min(100, (TwipsToPixelX(Win_1.Width) - 100) / threads)
For n = 1 To threads : DisplayBox(n, False) : Next n
For n = 1 To threads : th(n) = CreateThread(0, 0, ProcAddr(OtherThreads), n, CREATE_SUSPENDED, V:tid(n)) : Next n
For n = 1 To threads : ~ResumeThread(th(n)) : Next n
// Let the threads do their work
~WaitForMultipleObjects(threads, V:th(1), True, 10000)
// Signal for threads to close
exitall? = True
// Wait for threads to finish
~WaitForMultipleObjects(threads, V:th(1), True, 10000)
~CloseHandle(mh) : ~CloseHandle(sm)
Procedure DisplayBox(tn%, on?)
Local th%, tw%, wait%, xp% = 40 + ((tn% - 1) * x)
// Wait for access to Win_1
wait% = WaitForSingleObject(mh, 500)
If wait = WAIT_OBJECT_0
Output = Win_1
Win_1.ForeColor = $FFFFFF : PBox 10, 10, 200, 25 : Win_1.ForeColor = 0
Text 10, 10, "Time: " & Format(Timer - t#, "0.00")
Win_1.ForeColor = Iif(on?, 255, 0)
PBox xp%, x, xp% + x - 1, (x * 2) - 1
Win_1.ForeColor = Iif(on?, 0, $FFFFFF)
tw% = Win_1.TextWidth(Trim(tn%)) : th% = Win_1.TextHeight(Trim(tn%))
Text xp% + ((x - tw%) / 2), x + ((x - th%) / 2), Trim(tn%)
Win_1.ForeColor = 0
Box xp%, x, xp% + x - 1, (x * 2) - 1
Win_1.Refresh : DoEvents
EndIf
~ReleaseMutex(mh)
EndProcedure
Procedure OtherThreads(tn%)
Local Int32 ct, wait
While ct < 10 And Not exitall?
wait = WaitForSingleObject(sm, 1000)
If wait = WAIT_OBJECT_0
DisplayBox(tn%, True)
Delay .5
DisplayBox(tn%, False)
Inc ct
~ReleaseSemaphore(sm, 1, Null)
EndIf
Wend
~ExitThread(0)
EndProcedure
It should be noted that, as with Critical Section and Mutex, there is no logical order to which thread gets access to the Semaphore; in fact, the first few threads equal to the maximum number of threads seem to be given far greater priority than any of the others. Once again, this behaviour can be controlled by use of a global variable count as shown in the second example covering Critical Sections.
What follows are a few tips when things do not go quite right, although it is by no means an exhaustive list; in addition, irretrievable errors can be reduced by placing a Try/Catch structure within the main body of the program and its subsidiary procedures: this seems to work most of the time to prevent an irretrievable crash; however, on many occasions when an error or break (Ctrl-Break) is caught, you will need to save any changes, close and then restart the IDE before you are able to run your program again.
The Debug object (including the Trace command) is very useful for debugging but needs to be used with care when multithreading. It runs as a separate thread to the GFA Basic IDE but is not protected by a Critical Section or other such device so that simultaneous calls from different threads will cause a fatal error. See Restricting and Controlling Thread Access for a workaround for this.
Make sure windows, handles and arrays created locally in threads are destroyed and/or closed before the thread is exited.
If a program fails when an auxiliary thread tries to access a variable, make sure that that variable is either local to the thread or global to the whole program: trying to access variables local to other threads or the main program will result in a fatal error.
Another cause of crashes is when threads perform multiple concatenating or in other ways change the length of global string variables and arrays: this causes the string element to be destroyed and recreated at, possibly, a different memory location and, with arrays, the shunting up or movement of all successive elements which will cause an error if a different thread is currently accessing one of them; a workaround is to copy values to local variables, do the changes, then copy them back to the global array within a restricted access structure (see above).
If a thread fails to start - usually caused by an error in the CreateThread() function - sometimes, no matter what you do, it just doesn't seem to want to start working again, even if you revert any changes you have made which caused the error or non-function of the thread. This can sometimes be solved by closing all occurences of GFA Basic (saving first, of course) and then restarting your application.
This can also solve problems which appear to get worse on repeated execution of a program, such as windows not being drawn, etc. These latter issues are usually caused by threads not being properly closed before the IDE finishes execution of the main thread and sometimes do not occur in a compiled executable version of the problem.
Another cause of an unusual failure of a program in the IDE which has worked flawlessly in the past is if there are other instances of the GFA Basic IDE open in the background. Why this should be is unknown, but in many instances closing these other programs makes the problem go away.
If an auxiliary thread renders graphics or text to a Window which was opened by a different thread, it does not have ownership of that window and trying to change Font attributes can cause failures, although they tend simply to freeze or prevent the rendering rather that invoking a fatal error.
If rendering is slow or non-existent for any other reason, try using the DoEvents command or Refresh method to invoke a redraw immediately following the display command; also, for rendering non-OCX objects, make sure that the form's AutoRedraw property is set to 1 (one). However, keep in mind that adding DoEvents to a thread which does not 'own' a window can cause a crash; you can try using Form().Refresh instead although this does not always have the desired effect.
Similar to strings, data for I/O channels seems to be stored globally and opening and closing them from within threads shifts that data up and down in the internal tables; consequently, this will cause crashes if the location of data for an I/O channel being used by a thread moves, resulting in an incorrect channel being closed or an attempt made to close a channel that does not exist. It should be noted that this problem is not just limited to channels created using Open but also those opened internally when commands such as Bload and BSave are used.
A workaround for this is to have all channels that are required open before the different threads do whatever it is they are designed to do, and then close the channels en masse once all work has been performed; another option is to use a user-defined gateway similar to this example; a final option is to use a restricted access structure (see above) to ensure that only one channel is open and being used at any one time.
Next: Dpi-aware applications.
Back to: Creating an Application.
{Created by James Gaite; Last updated: 26/11/2023 by James Gaite}