|
|
||||||||||
|
|||||||||||
The Purpose and Usage of ThreadsA previous article dealt with the differences between processes and threads. This article addresses the motivation for using threads, how they are created, and how they are used. The terms task and thread are used interchangeably in this article.What is a Thread?A thread is a function that can run as a separate task, yet still share global data with the rest of the application. Any function can be started as a thread by making a system call to inform the kernel that you want "functionA" to start up as a separate thread of execution. The C function is just the starting point; it can call other functions which call other functions, and so forth. The thread can be suspended or interrupted at any point, e.g., you can be 3 function calls deep and issue a pause, which will suspend the thread for the indicated number of milliseconds. When the thread resumes, it will resume where it left off, 3 function calls deep. The term "thread of execution" is appropriate, since once a thread is started, it can meander through various levels of code and function calls, and regardless of where it is, it can be suspended and resumed where it left off.How Are Threads Created?As a previous article shows, it is often necessary in real-time applications to break an application down into multiple threads of execution which we will call tasks.
void taskA(void) {
while (TRUE) {
/* Task code here... */
}
}
void main(void)
{
startTask(makeTask(taskA, 0));
startTask(makeTask(taskB, 0));
startTask(makeTask(taskC, 0));
suspend();
}
In the code fragment above, taskA, taskB, and taskC are C functions. makeTask creates a task control block for the function. The system call startTask does the following.
Eventually a task voluntarily relinquishes control or is preempted, at which point a context switch occurs and the task at the front of the ready queue becomes the new active task. (For complete details about how a context switch occurs, see our book The Art of Real-time Programming.). Note that the task contains an infinite "while" loop, which means that it will never terminate. However, tasks do typically relinquish control either voluntarily (by using pause, yield, waitMsg, waitMail, etc.) or involuntarily (when the task is preempted by a higher priority task, or by time-slicing if time-slicing is enabled), and in this way other tasks will run in spite of the infinite loop.
Each Task has its Own StackIt is significant that each task has its own stack. The reasons for this are as follows:
#define BEGIN 1000
void ioTask(void)
{
int myIoAddress;
typeMsg * msg;
msg = waitMsg(BEGIN);
myIoAddress = msg->iData;
freeMsg(msg);
while (TRUE) {
pause(100L);
doIo(myIoAddress);
}
}
void main(void)
{
int i;
int ioAddresses[] = {0x1204, 0x1306, 0x1408};
typeMsg * msg;
typeTcb * tcb;
for (i = 0; i < 3; i++) {
tcb = startTask(makeTask(ioTask, i));
msg = makeMsg(tcb, BEGIN);
msg->iData = ioAddresses[i];
sendMsg(msg);
}
suspend();
}
"main" above starts 3 instances of the task ioTask, and each instance is sent a message named BEGIN which contains an IO address which that particular instance is to use for all IO operations. The task ioTask characterizes itself by saving its IO address in a local variable named "myIoAddress". Note that there will be 3 instances of the task ioTask running, each one accessing the variable "myIoAddress". There is no conflict, however, because each task instance has its own stack, and therefore, its own copy of local data.
It is important to understand that each task instance uses the same code, which in the example above is the function ioTask. That is, although each task gets its own stack, and therefore its own private local data, function call nesting information, and context save area, the actual task machine instructions are shared between all the task instances. This works because the code is generic, that is, it only references data on the stack, and therefore references only data for that instance. In contrast, the code below would cause a problem.
void ioTask(void)
{
static int myIoAddress;
typeMsg * msg;
msg = waitMsg(BEGIN);
myIoAddress = msg->iData;
freeMsg(msg);
while (TRUE) {
pause(100L);
doIo(myIoAddress);
}
}
This version of ioTask defines myIoAddress as a static, which means that there is only one copy of myIoAddress. In this situation, the following will occur.
However, this does not preclude the use of static or global data by task instances. The following code fragment has the same effect as the previous example, but uses global data to accomplish it.
int IoAddresses[] = {0x1204, 0x1306, 0x1408};
void ioTask(void)
{
int myIoAddress;
myIoAddress = IoAddresses[ActiveTcb->taskNum];
while (TRUE) {
doIo(myIoAddress);
pause(100L);
}
}
void main(void)
{
int i;
for (i = 0; i < 3; i++) {
startTask(makeTask(ioTask, i));
}
suspend();
}
The second argument to makeTask is the task number, which is an arbitrary number chosen by the caller that is assigned to tcb->taskNum for that instance. As mentioned previously, all instances share the same code (ioTask in this example), however, the particular task instance can be identified by referencing the global ActiveTcb->taskNum, which contains the task number (second argument to makeTask). ActiveTcb always points to the tcb of the currently running (active) task. The task ioTask uses ActiveTcb->taskNum as an index into a global table to access the ioAddress for that particular instance.
Why use a Thread Instead of a Process?A real-time software system is typically a single application that requires concurrency like a data acquisition application that must sample data, display data, accept keyboard input, and process incoming RS-232 messages concurrently. In this case multi-tasking is required, but all tasks are common to the same application. Contrast this with a multi-tasking PC environment that has many different applications (processes) like a word processor, an internet browser, and a database that may all run concurrently. For example, while the database is busy doing a time consuming sort, the user can use the word processor.Processes, however, are typically autonomous; that is, they are typically not dependent upon one another, and therefore, do not share data, nor do they need to synchronize with one another. However, since threads are part of the same application, they typically need to share data and synchronize. For example, in a data acquisition application, let us assume that taskA reads external data, while taskB displays data each time new data is available. taskB waits for a message named NEW_DATA_AVAILABLE, which means that taskB will suspend until it receives the message NEW_DATA_AVAILABLE. When taskA has data available it sends a NEW_DATA_AVAILABLE message to taskB which will wake up taskB so that it can receive the message. How taskB gets the data is a matter of design. taskB could obtain the new data in any of the following ways.
Finally, as a side note, processes can in fact share data if the operating system and hardware allow for it, but for small embedded applications, the operating system, or the hardware may not have the ability to accomplish this and therefore, processes cannot be created. For more information on processes and threads see a previous article.
We welcome comments. Let us know what subjects you would like written up.
Send comments to Mike@TicsRealtime.com
Copyright © 2000, Tics Realtime
| |||||||||||