Using multiple threads Foundation
Multi-threading is a special characteristic of the operating system which allows an application program to be divided into various components which can be executed independently and simultaneously. The classic example for this is an application that provides the ability to evaluate data and print reports from one database while data input in another database is occurring. The idea is that the user starts one time-consuming procedure, and immediately begins working with another procedure while the first one is still running. In this example, the evaluation and reporting on one database runs in a different thread than the routine for data input. However, the database evaluation and report procedure, and the data input procedure are components of the same application. Another example is a program for data collection where the data is collected from various sources, with each source being controlled by a separate thread. Even when data is being received simultaneously from different sources, it can be reliably recorded.
An application program (the EXE file) is started as a process. A process consists of one or more threads. Within a process, a thread can be thought of as a separate execution path where functions and procedures are executed independently from other threads. When a process consists of several threads, the operating system allocates the microprocessor (CPU) time for the different threads. Which thread gains access to the CPU (which thread is executed) depends first on the priority of a thread and then on whether it should execute instructions or whether it is currently in an idle mode. In multi-threading, the operating system allots each thread a limited amount of CPU time (called the time slice), and each thread is given its time in turn. In this way several processes are executed at the same time (multi-tasking), and within a process several threads can be executed (multi-threading). However, from the point of view of the CPU, only one thread is executing at any point in time.
The Thread class of Xbase++ offers the programmer a tool for taking advantage of multi-threading in a simple straightforward manner. A thread object must be created and the object must receive information as to what program code to execute in the thread. The following example shows the basic approach:
The program has the sole purpose of demonstrating the use of a thread object in a short example (otherwise it is meaningless). The thread object is created using the :new() method of the Thread class. As soon as a thread object is created, a new thread is available. The thread is then ready to run. The program code to be executed in the new thread is specified by calling the :start() method of the thread object. The first parameter is a character string specifying the identifier for the function or procedure to be executed in the thread. In the example, the user-defined function Sum() is specified. All other parameters (10000 in this case) are passed as arguments to the function being executed. After the method :start() is called, the specified program code is executed in the new thread.
In the example program, two loops run simultaneously. The DO WHILE loop in the Main procedure outputs a dot on the screen on each pass through the loop. The FOR...NEXT loop in the function Sum() simultaneously calculates a sum. When the FOR...NEXT loop in the new thread is terminated, the DO WHILE loop is also terminated, because the instance variable :active signals that the new thread is no longer executing code. The result of the calculation is contained in the :resultinstance variable of the thread object.
The thread is created when the thread object is created. Execution of code in the thread is started using the method :start(). The identifier for the function or procedure to be executed in the thread is passed to this method as a character string. The symbol or identifier for the function or procedure must be available at runtime. This means the function or procedure started in a new thread cannot be declared as a STATIC FUNCTION or STATIC PROCEDURE. As long as a thread is executing program code, the instance variable :active has the value .T. (true). When the thread has terminated, the return value of the last function or procedure executed in the new thread is assigned to the instance variable :result.
The ability to divide a program up into different threads presents a new dimension for programmers who have not previously programmed in a multi-threading environment. Although creating threads is simple in Xbase++, programming a multi-threaded application adds new complexity and requirements for the design of programs. In addition, new sources of error must be considered which may result from different parts of an application being executed at the same time. First of all, multi-threading affects the visibility of variables in different threads. The following table shows the differences:
Storage class | Visibility |
---|---|
PUBLIC | process-wide (all threads) |
STATIC | process-wide (all threads) |
LOCAL | thread-local (this Thread) |
PRIVATE | thread-local (this Thread) |
FIELD | thread-local (this Thread) |
Variables declared as LOCAL or PRIVATE are only visible in the thread where the declaration occurred. The variables declared with PUBLIC or STATIC are visible in all threads of a process (application program). Field variables (FIELD) are visible in a work area of a work space. A work space is bound to a thread. Since work spaces can be moved between threads, field variables can become visible in different threads. At a given point in time, a field variable is visible only in one thread.
Whenever program code is divided up into different threads, the possibility of multiple threads having simultaneous access to the same variable (PUBLIC or STATIC) and changing it should be avoided. If multiple threads are modifying the same variable, the value of the variable is not predictable. The following example demonstrates this situation:
In the example, two FOR...NEXT loops run simultaneously in two different threads. The same STATIC variable is accessed in both threads. The first thread increments the variable 10000 times and the second thread decrements the variable 10000 times. Theoretically, the result would be the value zero. In practice this value is seldom reached. Generally the value of snNumber at the end of the program is greater than zero. This is because the operating system independently allocates the processor time for the two threads. The FOR...NEXT loop in the Main procedure begins incrementing as soon as the first thread is started. Switching between the threads takes time, and the STATIC variable increments several times before the second thread has actually started. When the FOR...NEXT loop in the first thread ends, the entire process is terminated, including the second thread. This means that the FOR...NEXT loop in the second thread is cancelled before the counter variablei reaches the value 10000. For this reason, the value of snNumber at the end of the program is almost always greater than zero.
This program demonstrates that programming multiple threads requires consideration of special issues. Simultaneous access to the same variables or files by multiple threads should be avoided. When two threads are using the same variables, the result (or the value of the variable) is not predictable, since the operating system allocates which thread is to receive available processor time. The thread which last performed an assignment sets the value of the variable. As a general rule, the part of the program that is to run in a separate thread should be written in such way that it can be compiled and linked as an independent program. All variables in a thread should be protected from access by other threads.
Multi-threading allows different programs to run at the same time or the same program code to be simultaneously executed multiple times in different threads. When a value is assigned to a variable, the value of the variable depends on the thread which is allocated processor time by the operating system. The processor time allocated to a thread can be influenced by its defined priority. So the result of the last example program can be changed if a single program line is added:
In this example, the priority of the new thread is increased in relation to the current thread using the method :setPriority(). This causes processor time to be preferentially allocated to the new thread. This means that the FOR...NEXT loop in the Decrement procedure is processed first, since the thread in which this loop runs has a higher priority than the thread in which the Main procedure is running. In this case, the FOR...NEXT loop in the Decrement procedure runs before the FOR...NEXT loop in the Main procedure. The result of the program is always zero because snNumber is first decremented 10000 times and then incremented 10000 times.
The example represents an extreme case in which the order of execution can be precisely controlled by raising the priority of individual threads. Generally, the order of execution of threads (the allocation of processor time) depends on several factors which are controlled by the operating system.
By default, threads in an Xbase++ program have the priority PRIORITY_NORMAL and this is generally adequate. This results in an Xbase++ application being given processor time on equal precedence with most other programs. In normal situations, the priority should not be changed. Changing the priority requires a detailed knowledge of the manner in which the operating system distributes processor time. Threads receive processor time based on their priority. Low priority threads receive CPU access if no thread with higher priority is running or if a higher priority thread has entered a wait state. A higher priority may be temporarily assigned to a thread with a lower priority to allow it to be executed (starvation boost).
The thread object allows the priority of threads to be changed and it remains the programmer's responsibility to use this power responsibly. Raising the priority of threads only provides more processor time to the thread from the operating system. It does not cause the program to run faster. In the worst scenario, if the priority is set too high, multi-tasking and multi-threading are no longer possible, since the Xbase++ application (or a single thread in the Xbase++ application) is allocated all the processor time. In this case, other programs cannot run until the Xbase++ application has terminated. Changing the priority of threads demands special care. These settings directly influence preemptive execution of several programs, or processes, respectively (multi-tasking). They do not affect the performance of an individual Xbase++ application.
Two functions exist in Xbase++ which are very useful in the context of multi-threading. They are used in the implementation of program code where the thread object which executes this code is unknown. The functions are ThreadID() and ThreadObject().
Each thread managed by a Thread object can be identified by a numeric ID. Thread IDs are consecutive numbers, i.e. the first thread has the ID 1 and it executes the Main procedure. A Thread object stores the thread ID in its instance variable :threadID. When the function ThreadID() is called, it retrieves the Thread object of the current thread and returns the value of the instance variable :threadID. This again is the numeric ID of the current thread.
The function ThreadObject() is used in a similar way. But instead of the numeric ID, it returns the complete Thread object which executes the function. Therefore, the result of the following expressions is always identical:
A thread is started by calling the :start() method of a Thread object. Normally, execution of program code within the thread begins immediately after the method is called. However, it is possible to define the exact time when the thread is to begin with program execution. This is done with the :setStartTime() method which must receive a numeric value indicating "seconds since midnight". Example:
A Thread object monitors the system timer. Therefore, the routine HighNoon() in the example is executed at 12 o' clock although the thread is started earlier. The current thread which has called the :start() method continues to run.
Another form of time-dependent execution of program code is provided by the :setInterval() method. It defines a time interval for repeated execution of program code by a Thread object. Each time the interval expires, the Thread object automatically restarts its thread. This functionality is also provided in a simplified form by the function SetTimerEvent(). A typical example for this is the continuous display of the current time which can be programmed in different ways:
The result of all three examples is identical: the time is displayed once a second in the upper left corner of the screen (the unit for the time interval is 1/100ths of a second). The easiest implementation is given by the SetTimerEvent() function which repeatedly evaluates a code block.
A comparison of the procedures ShowTime_A() and ShowTime_B() reveals an important implication which results from using the method :setInterval(). Example #2 uses a DO WHILE loop and an explicit wait state (function Sleep()) for continuous display, while example #3 works continuously without a DO WHILE loop. In example #3, a time interval which is monitored by the Thread object is defined. Therefore, the procedure ShowTime_B() is executed each time the interval elapses, and the thread implicitly enters a wait state in between two execution cycles.
The program code invoked in a thread by calling the :start()method can be differentiated in greater detail by additional (de)initialization routines which are executed once at the beginning of a thread and once before it terminates. The instance variables :atStart and :atEnd of a Thread object serve this particular purpose. Both can be assigned names of functions or code blocks:
In this example, a database query which lists data of all customers living in New York is programmed. The file is opened when the thread starts, i.e. before the query begins, and it is closed before the thread terminates. This occurs in the :atStart and :atEnd code blocks. The program code for the evaluation of the database is implemented without the typical DO WHILE .NOT. Eof() loop. This becomes possible because the time interval for repeated execution of this code is set to zero. As a result, the code is immediately started again whenever the RETURN statement is reached. When the record pointer is moved to the end of file (Eof() == .T.), the interval is set to NIL which causes the thread not to repeat the code but to terminate.
The Thread class can serve as superclass for user-defined Thread classes whose instances each have their own thread. Three methods are provided for use in derived Thread classes. They have the PROTECTED: visibility attribute and can therefore be used in subclasses only. These methods are :atStart(), :execute() and :atEnd(), of which at least the :execute() method must be programmed in a user-defined Thread class. It contains the code to be executed in the separate thread after the :start() method is called.
The example for the database query in the previous section is used as basis for the following Thread class which performs the same database operations:
The user-defined class is instantiated and a code block with the condition for the query is passed to the :start() method. The three methods :atStart(), :atEnd() and :execute() are then automatically invoked and executed within the thread. The code block passed to :start() is also passed to the method :execute(). The code of this method is repeatedly executed until the end of file is reached.
The example shows the methods which can or must be implemented in a user-defined Thread class. It uses the mechanism for time-controlled repeated execution of code in a thread. Instead of :setInterval( 0 | NIL ), a DO WHILE loop can be used as well.
In multi-threaded programs, each thread can be viewed as a separate execution path in which different parts of a program may be executed at the same time. It is also possible to run one and the same part of a program simultaneously in multiple threads. A DO WHILE loop, for example, can be programmed once but may be executed 10 times at the same time. Therefore, all language elements which control program flow in one thread are not appropriate for controlling the program flow between multiple threads. This applies to statements like FOR..NEXT, DO WHILE..ENDDO, IF..ENDIF, DO CASE..ENDCASE or BEGIN SEQUENCE..ENDSEQUENCE. All of these control structures are translated by the compiler at compile time and are only valid for one thread.
The possibilities for coordinating different threads begin with halting the current thread until one or more other threads have terminated. The functions ThreadWait(), ThreadWaitAll() and the method :synchronize()of the Thread class are used for this purpose. Whenever one of these functions or the method is called, the current thread stops program execution and enters a wait state. The thread waits for the end of one or more other threads and resumes afterwards. While waiting, the thread consumes no CPU resoures. The following scheme demonstrates this:
Thread A starts thread B and waits for its end at a particular point in the program by calling the :synchronize() method. It is not possible to terminate thread B explicitly from thread A.
Normally, the coordination of threads via wait states is necessary if one thread A needs the result of another thread B. For example, the calculation of extensive statistics can be done in multiple threads where each thread collects data from a particular database and calculates just one part of the statistic. The consolidation of the entire statistic then occurs in one single thread which needs to wait for the results of all other threads. In this scenario, all threads execute different parts of a program at the same time and must be coordinated or synchronized at a particular point in the program. The coordination can be implemented by one thread waiting for all others, or by one thread telling other threads to leave their wait state. The latter possibility requires a Signal object for communication between threads.
There is a possibility for coordinating threads which does not require a thread to terminate before another thread resumes. However, this requires the usage of an object of the Signal class. The Signal object must be visible in two threads at the same time. With the aid of a Signal object, one thread can tell one or more other threads to leave their wait state and to resume program execution:
Whenever a thread executes the :wait() method of a Signal object, it enters a wait state and stops program execution. The thread leaves its wait state only after another thread calls the :signal() method of the same Signal object. In this way a communication between threads is realized. One thread tells other threads to leave their wait state and to resume program execution.
As long as multiple threads execute different program code at the same time, the coordination of threads is possible using wait states as achieved with:synchronize() or ThreadWait(). However, wait states are not possible if the same program code is running simultaneously in multiple threads. A common example for this situation is adding/deleting elements to/from a globally visible array:
In this example, the array aQueue is used to temporarily store arbitrary values. The values are retrieved from the array according to the FIFO principle (First In First Out). Function Add() adds an element to the end of the array, while function Del() reads the first element and shrinks the array by one element.
When the functions Add() and Del() are executed in different threads, the PUBLIC array aQueue is accessed simultaneously by multiple threads. This leads to a critical situation in function Del() when the array has only one element. In this case, a runtime error can occur:
The operating system can interrupt a thread at any time in order to give another thread access to the CPU. If threads A and B execute function Del() at the same time, it is possible that thread A is interrupted immediately after the IF statement. Thread B may then run the function to completion before thread A is scheduled again for program execution. In this case, a runtime error can occur because the function Del() is not completely executed in one thread before another thread executes the same function.
The example function Del() represents those situations in multi-threading which require muliple operations to be executed in one particular thread before another thread may execute the same operations. This can be resolved when thread B is stopped while thread A executes the Del() function. Only after thread A has run this function to completion may thread B begin with executing the same function. Such a situation is called "mutual exclusion" because one thread excludes all other threads from executing the same code at the same time.
Mutual exclusion of threads is achieved in Xbase++ not on the PROCEDURE/FUNCTION level but with the aid of SYNC methods. The SYNC attribute for methods guarantees that the method code is executed by only one thread at any time. However, this is restricted to one and the same object. If one object is visible in multiple threads and the method is called simultaneously with that object, the execution of the method is serialized between the threads. In contrast, if two objects of the same class are used in two threads and the same method is called with both objects, the program code of the method runs parallel in both threads. As a result, mutual exclusion is only possible if two threads attempt to execute the same method with the same object. The object must be an instance of a user-defined class that implements SYNC methods. A SYNC method is executed entirely in one thread. All other threads are automatically stopped when they attempt to execute the same method with the same object.
The example with the beforementioned PUBLIC array aQueue must be programmed as a user-defined class in order to safely access the array from multiple threads:
In this example, the Queue class is used for managing a dynamic array that may be accessed and changed from multiple threads simultaneously. The array is referenced in the instance variable :aQueue and it is accessed within the SYNC methods :add() and :del(). The Queue object which contains the array is globally visible. The execution of the :del() method is automatically serialized between multiple threads:
When thread B wants to execute the :del() method while this method is executed by thread A, thread B is stopped because it wants to execute the method with the same Queue object. Therefore, SYNC methods are used whenever multiple operations must be guaranteed to be executed in one thread before another thread executes the same operations. A SYNC method can be executed with the same object only in one thread at any time.
If you see anything in the documentation that is not correct, does not match your experience with the particular feature or requires further clarification, please use this form to report a documentation issue.