This chapter describes how to use ChorusOS operating system services to create a multithreaded actor. It contains the following sections:
"Basic Multi-Thread Programming" is an overview of the multithreading model of the ChorusOS operating system.
"Thread Handling" explains how to identify, create, and delete a thread.
"Synchronizing Threads" explains the available methods for synchronizing threads.
"Basic Scheduling Control" explains how to schedule threads.
"Managing Per-Thread Data" explains how to maintain and use per-thread and shared data.
"Threads and Libraries" explains how to use libraries within a multithreaded actor.
Within an actor, whether user or supervisor, one or more threads may execute concurrently. A thread is the unit of execution in a ChorusOS operating system and represents a single flow of sequential execution of a program. A thread is characterized by a context corresponding to the state of the processor (registers, program counter, stack pointer or privilege level, for example). See Figure 6-1.
Threads may be created and deleted dynamically. A thread may be created in another actor than the one to which the creator thread belongs, provided they are both running on the same machine. The actor in which the thread was created is named the home actor or the owning actor. The home actor of a thread is constant during the life of the thread.
The system assigns decreasing priorities to boot actor threads, so that boot actor main threads are started in the order in which they were loaded into the system image. If a boot actor's main thread sleeps or is blocked, the next boot actor threads will be scheduled for running.
Although there are no relationships maintained by the ChorusOS operating system between the creator thread and the created thread, the creator thread is commonly called the parent thread, and the created thread is commonly called the child thread.
A thread is named by a local identifier referred to as a thread identifier. The scope of this type of identifier is the home actor. In order to name a thread of another actor, you must provide the actor capability and the thread identifier. It is possible for a thread to refer to itself by using the predefined constant: K_MYSELF.
All threads belonging to the same home actor share all the resources of that actor. In particular, they may access its memory regions, such as the code and data regions, freely. In order to facilitate this access, the ChorusOS operating system provides synchronization tools which are covered in a later section of this document.
Threads are scheduled by the kernel as independent entities; the scheduling policy used depends on the scheduling module configured within the system. In a first approach, assume that a thread may be either active or waiting. A waiting thread is blocked until the arrival of an event. An active thread may be running or ready to run.
A thread may obtain its local identifier by means of the following ChorusOS operating system service:
#include <chorus.h> int threadSelf();
An example of how this call can be used is provided in Example 6-1.
A thread may be created dynamically by means of the following ChorusOS operating system service:
#include <chorus.h> int threadCreate(KnCap* actorCap, KnThreadLid* thLi, KnThreadStatus status, void* schedParam, void* startInfo);
The actorCap parameter identifies the actor in which the new thread will be created. You can create the new thread in the current actor by passing K_MYACTOR as the actor capability. This is the usual case. Should this be successful, the local identifier of the newly created thread is returned at the location defined by the thLi parameter.
The schedParam parameter is used to define the scheduling properties of the thread to be created. If this parameter is set to 0, the created thread inherits the scheduling attributes of the creator thread.
The startInfo parameter is used to define the initial state of the thread, such as the initial program counter of the thread (the thread entry point), as well as the initial value of the stack pointer to be used by the created thread. You can also define whether the thread will run as a user thread or as a supervisor thread.
A thread needs a stack to run, in order to have room to store its local variables. When the thread is a user thread, the user must explicitly provide a stack to the thread. However, stacks for supervisor threads are implicitly allocated by the system. In fact, a system stack is allocated for all threads, even those running in user mode.
As the operating system does not prevent the user stack from overflowing, checks must be made every time a thread is created.
System stacks are not allowed to overflow as memory will become corrupted, resulting in unpredictable operating system behavior.
Example 6-1 is a simple program illustrating the creation of a thread by the main thread of an actor. The actor is loaded by the arun command. Its main thread is implicitly created by the system. The goal of the example is to:
create a thread, which prints a message, including its thread identifier
simultaneously, the main thread prints another message with both thread identifiers
the main thread then terminates the actor
This example will work without modification whether it is run as a user or as a supervisor actor. In the first case, a user thread must be created, while in the second case a supervisor thread must be created. Using the actorPrivilege() service call might be helpful for this purpose.
This example requires some kind of synchronization between the main thread and the created one. Execution of a thread can be suspended for a given delay:
#include <chorus.h> int threadDelay(KnTimeVal* waitLimit);
This call suspends the execution of the invoking thread for a period specified by the KnTimeVal structure (see "Current Time " for more detail). There are two predefined values:
K_NOTIMEOUT specifies an infinite delay.
K_NOBLOCK, which specifies no delay. This is an explicit request for the processor to yield and reschedule another thread of the same priority.
These values may be used instead of the pointer to the KnTimeVal data structure. There is also a predefined macro which sets such a structure from a delay expressed in milliseconds: K_MILLI_TO_TIMEVAL(KnTimeVal* waitLimit, int delay). For more information, see the threadCreate(2K), threadDelay(2K), and threadSelf(2K) man pages.
(file: progov/thCreate.c) #include <stdio.h> #include <stdlib.h> #include <chorus.h> #define USER_STACK_SIZE (1024 * sizeof(long)) int childCreate(KnPc entry) { KnActorPrivilege actorP; KnDefaultStartInfo_f startInfo; char* userStack; int childLid = -1; int res; /* Set defaults startInfo fields */ startInfo.dsType = K_DEFAULT_START_INFO; startInfo.dsSystemStackSize = K_DEFAULT_STACK_SIZE; /* Get actor's privilege */ res = actorPrivilege(K_MYACTOR, &actorP, NULL); if (res != K_OK) { printf("Cannot get the privilege of the actor, error %d\n", res); exit(1); } /* Set thread privilege */ if (actorP == K_SUPACTOR) { startInfo.dsPrivilege = K_SUPTHREAD; } else { startInfo.dsPrivilege = K_USERTHREAD; } /* Allocate a stack for user threads */ if (actorP != K_SUPACTOR) { userStack = malloc(USER_STACK_SIZE); if (userStack == NULL) { printf("Cannot allocate user stack\n"); exit(1); } startInfo.dsUserStackPointer = userStack + USER_STACK_SIZE; } /* Set entry point for the new thread */ startInfo.dsEntry = entry; /* Create the thread in the active state */ res = threadCreate(K_MYACTOR, &childLid, K_ACTIVE, 0, &startInfo); if (res != K_OK) { printf("Cannot create the thread, error %d\n", res); exit(1); } return childLid; } void sampleThread() { int myThreadLi; myThreadLi = threadSelf(); printf("I am the new thread. My thread identifier is: %d\n", myThreadLi); /* Block itself for ever */ threadDelay(K_NOTIMEOUT); } int main(int argc, char** argv, char**envp) { int myThreadLi; int newThreadLi; int res; KnTimeVal wait; newThreadLi = childCreate((KnPc)sampleThread); myThreadLi = threadSelf(); /* Initialize KnTimeVal structure */ K_MILLI_TO_TIMEVAL(&wait, 10); /* * Suspend myself for 10 milliseconds to give the newly * created thread the opportunity to run before * the actor terminates. */ res = threadDelay(&wait); printf("Parent thread identifier = %d, Child thread identifier = %d\n", myThreadLi, newThreadLi); return 0; }
The schedParam parameter is set to 0. As a result, the created thread will inherit the scheduling attributes of the creator thread.
Note the usage of the actorPrivilege() service which enables the program to determine whether it must allocate a user stack area for the created thread or not, as well as to indicate the type of thread to be created.
If the actor is a supervisor actor, the following line: startInfo.dsSystemStackSize = K_DEFAULT_STACK_SIZE only gives an indication to the system of the expected usage of the system stack. The maximum system stack length is defined by a global tunable value.
On some platforms the stack pointer value passed in dsUserStackPointer is automatically decremented by the kernel before being used for the thread. This is done either to enforce the platform-required alignment, on 8 or 16 byte boundaries for example, or to reserve a space which will be accessed by a typical C language routine because of the platform-specific calling conventions, such as saving the return address to the caller.
The status parameter is used to create the thread in the active state, so that the thread is ready to execute as soon as it is created.
Be aware that, although this program explicitly creates only one thread, there are in fact two threads running in this actor: the main thread created implicitly by the system when the actor is loaded, and the thread explicitly created by the program.
The above example uses a service named threadDelay(), which allows a thread to suspend its execution for a certain period. The parent thread suspends itself for ten milliseconds, so that the child thread is able to run before exit is called. Without this suspension period in the parent thread, the actor could terminate before the created thread has run.
As explained earlier, the termination of an actor implies that all its resources are freed. Threads are not an exception to that rule. Thus, the exit() call at the end of the main routine will lead to the destruction of both threads. Use of the period within the parent thread is not a guarantee. Depending on the load of the system, ten milliseconds might not be sufficient to ensure that the child thread has completed its task. The threadDelay() has only been used in this example for the sake of simplicity, and is not recommended in practice for synchronizing threads. A more reliable synchronization scheme should be used to be sure that the actor does not terminate before the second thread has completed all jobs. These synchronization mechanisms are explained in "Synchronizing Threads".
The child thread uses the K_NOTIMEOUT special value to suspend itself for ever. This is a simple way to avoid undesirable behavior of the child thread until the actor terminates. Assume this call to threadDelay() does not exist. The child thread, after having executed the printf() statement, would reach the end of the sampleThread() routine, which being written in C terminates with a return instruction. However, the child thread has nowhere to return. As a result it would return to an unspecified location, probably resulting in a memory fault.
The system does not preset the stack of a thread to ensure that the thread is deleted upon return from its starting routine. You, the ChorusOS operating system programmer, must ensure that threads are properly cleaned up after they finish running. Mechanisms for coping with these types of situations are described in Chapter 7, Memory Management.
A thread may be dynamically deleted by itself or by another one using the following service:
#include <chorus.h> int threadDelete(KnCap* actorCap, KnThreadLid thLi);
This call enables a thread to delete another one inside the same actor, when actorCap is set to K_MYACTOR, by knowing the thread identifier of the thread to be deleted. It also enables a thread to delete another one inside another actor (provided they are both running on the same machine), as long as it provides both the actor capability and the target thread identifier. The predefined thread identifier K_MYSELF enables a thread to name itself without knowing its actual thread identifier.
Example 6-2 is a slightly different version of the previous program. The subroutine childCreate() is unchanged, but now the created thread kills itself, instead of going idle forever.
This does not solve the synchronization problem occurring in the previous example: the main thread still does not know exactly when to terminate the actor.
Refer to the threadDelete(2K) man page.
(file: progov/thDelete.c) #include <stdio.h> #include <stdlib.h> #include <chorus.h> #define USER_STACK_SIZE (1024 * sizeof(long)) int childCreate(KnPc entry) { KnActorPrivilege actorP; KnDefaultStartInfo_f startInfo; char* userStack; int childLid = -1; int res; startInfo.dsType = K_DEFAULT_START_INFO; startInfo.dsSystemStackSize = K_DEFAULT_STACK_SIZE; res = actorPrivilege(K_MYACTOR, &actorP, NULL); if (res != K_OK) { printf("Cannot get the privilege of the actor, error %d\n", res); exit(1); } if (actorP == K_SUPACTOR) { startInfo.dsPrivilege = K_SUPTHREAD; } else { startInfo.dsPrivilege = K_USERTHREAD; } if (actorP != K_SUPACTOR) { userStack = malloc(USER_STACK_SIZE); if (userStack == NULL) { printf("Cannot allocate user stack\n"); exit(1); } startInfo.dsUserStackPointer = userStack + USER_STACK_SIZE; } startInfo.dsEntry = entry; res = threadCreate(K_MYACTOR, &childLid, K_ACTIVE, 0, &startInfo); if (res != K_OK) { printf("Cannot create the thread, error %d\n", res); exit(1); } return childLid; } void sampleThread() { int myThreadLi; myThreadLi = threadSelf(); printf("I am the new thread. My thread identifier is: %d\n", myThreadLi); /* Suicide */ threadDelete(K_MYACTOR, K_MYSELF); /* Should never reach this point! */ } int main(int argc, char** argv, char**envp) { int myThreadLi; int newThreadLi; int res; KnTimeVal wait; newThreadLi = childCreate((KnPc)sampleThread); myThreadLi = threadSelf(); /* Initialize KnTimeVal structure */ K_MILLI_TO_TIMEVAL(&wait, 10); /* * Suspend myself for 10 milliseconds to give the newly * created thread the opportunity to run before * the actor terminates. */ res = threadDelay(&wait); printf("Parent thread identifier = %d, Child thread identifier = %d\n", myThreadLi, newThreadLi); return 0; }
The exit() function is used instead of the threadDelete() function in the main thread. Using threadDelete() would leave the actor in a passive situation, with no thread running within it. This implies that resources used by an actor are not freed when the last thread is deleted.
In the case of a user thread, deleting a thread does not imply that the stack of the thread will be freed. If the user stack was allocated through a call to malloc(), it must be freed through a call to free(). This cannot be done by the thread itself, it must be done by another thread. In the above example, the actor is going to terminate, so there is no real need to do this because all resources used by the actor will be returned to the system. In the case of a supervisor thread, the ChorusOS operating system frees the system stack it had allocated at threadCreate() time.
The previous section explained the need for threads to be synchronized accurately, avoiding using delays which are difficult to tune and which depend on the load of the system. The ChorusOS operating system offers various tools for synchronizing threads:
Semaphores, which are common counting semaphores that support the P and V operations. See "Semaphores".
Mutexes, which provide a convenient and efficient way to implement mutual exclusion between multiple threads, in order to prevent a critical section from being executed in parallel by different threads. See "Mutexes".
Thread semaphores, which may be used to block a single thread awaiting the arrival of an event.
Event flags, which may be useful when a thread has to handle multiple events, providing the kind of multiplexing which is offered by the select system call, but at a much lower level.
A semaphore is an integer counter associated with a queue, possibly empty, of waiting threads. At initialization, the semaphore counter receives a user-defined positive or null value. Initialization is performed by invoking the following ChorusOS operating system service:
#include <chorus.h> int semInit(KnSem* semaphore, unsigned int count);
The semaphore parameter is the location of the semaphore and count is the semaphore counter. The semaphore must have been previously allocated by the user: allocation is not performed by the ChorusOS operating system. This implies that semaphores may be freely allocated by the user where convenient for his applications. As data structures representing semaphores are allocated by the applications, the ChorusOS operating system does not impose any limit on the maximum number of semaphores which may be used within the system.
Two atomic operations, named P and V, are provided on these semaphores.
#include <chorus.h> int semP(KnSem* semaphore, KnTimeVal* waitLimit);
semP() decrements the counter by one. If the counter reaches a negative value, the invoking thread is blocked and queued within the semaphore queue. Otherwise the thread continues its execution normally. The waitLimit parameter may be used to control how long the thread will stay queued. If waitLimit is set to K_NOTIMEOUT, the thread will stay blocked until the necessary V operation is performed. In the case of the thread being awakened due to the expiration of the period, a specific error code is returned as the result of the semP() invocation. In this case, the counter is incremented to compensate for the effect of the semP() operation.
#include <chorus.h> int semV(KnSem* semaphore);
semV() increments the counter by one. If the counter is still lower than or equal to zero, one of the waiting threads is picked up from the queue and awakened. If the counter is strictly greater than zero, there should be no thread waiting in the queue.
Figure 6-2 shows an example of two threads synchronizing by means of a semaphore.
The following example is based on the previous one, but the two threads explicitly synchronize by means of a semaphore, so that the actor will eventually be destroyed when the created thread has done its job and as soon as it has done so. Refer to the semInit(2K) man page.
(file: progov/semaphore.c) #include <stdio.h> #include <stdlib.h> #include <chorus.h> #define USER_STACK_SIZE (1024 * sizeof(long)) KnSem sampleSem; /* Semaphore allocated as global variable */ int childCreate(KnPc entry) { KnActorPrivilege actorP; KnDefaultStartInfo_f startInfo; char* userStack; int childLid = -1; int res; startInfo.dsType = K_DEFAULT_START_INFO; startInfo.dsSystemStackSize = K_DEFAULT_STACK_SIZE; res = actorPrivilege(K_MYACTOR, &actorP, NULL); if (res != K_OK) { printf("Cannot get the privilege of the actor, error %d\n", res); exit(1); } if (actorP == K_SUPACTOR) { startInfo.dsPrivilege = K_SUPTHREAD; } else { startInfo.dsPrivilege = K_USERTHREAD; } if (actorP != K_SUPACTOR) { userStack = malloc(USER_STACK_SIZE); if (userStack == NULL) { printf("Cannot allocate user stack\n"); exit(1); } startInfo.dsUserStackPointer = userStack + USER_STACK_SIZE; } startInfo.dsEntry = entry; res = threadCreate(K_MYACTOR, &childLid, K_ACTIVE, 0, &startInfo); if (res != K_OK) { printf("Cannot create the thread, error %d\n", res); exit(1); } return childLid; } void sampleThread() { int myThreadLi; int res; myThreadLi = threadSelf(); printf("I am the new thread. My thread identifier is: %d\n", myThreadLi); res = semV(&sampleSem); if (res != K_OK){ printf("Cannot perform the semV operation, error %d\n", res); exit(1); } /* Suicide */ res = threadDelete(K_MYACTOR, K_MYSELF); if (res != K_OK){ printf("Cannot suicide, error %d\n", res); exit(1); } /* Should never reach this point! */ } int main(int argc, char** argv, char**envp) { int myThreadLi; int newThreadLi; int res; /* * Initialize the semaphore to 0 so that * the first semP() operation blocks. */ res = semInit(&sampleSem, 0); if (res != K_OK) { printf("Cannot initialize the semaphore, error %d\n", res); exit(1); } newThreadLi = childCreate((KnPc)sampleThread); myThreadLi = threadSelf(); printf("Parent thread identifier = %d, Child thread identifier = %d\n", myThreadLi, newThreadLi); /* * Since semaphore has been initialized to 0 * this semP will block until a semV is performed * by the created thread, letting the main thread know * that created thread's job is done. */ res = semP(&sampleSem, K_NOTIMEOUT); if (res != K_OK) { printf("Cannot perform the semP operation, error %d\n", res); exit(1); } /* * Created thread has run and done all of its job. * It is time to safely exit. */ return 0; }
The semaphore sampleSem is allocated as global data of the actor. As the address space of the actor is shared by all threads running within the actor, both threads can freely access the semaphore in order to synchronize.
Avoid performing the semaphore initialization after having created the child thread. Depending on the scheduling, the second thread may start its execution as soon as it is created, and could reach the semV() operation before the semaphore has been initialized. Although the semV() could appear to work, semP() will never return due the fact that semInit() would reset the counter to 0.
The synchronization will work whatever the order in which the semP() and semV() operations are done. If semP() is done first, the counter will be set to -1 and the main thread will be blocked. The semV() will awake the main thread. If scheduling is reversed, the semV() will set the counter to 1, so that when the semP() operation occurs, the counter will be decremented to 0, but the thread will not block.
Assume that the two threads need to access one or more global variables in a consistent fashion. A simple example could be that each of the threads needs to add two numbers to a unique global counter. Whatever the scheduling may be, the unique global counter should always reflect the accurate sum of all numbers added by both threads.
This could be done using semaphores. However, the ChorusOS operating system provides mutexes which have been specifically designed and tuned for these types of needs.
A mutex is a binary flag associated with a queue, possibly empty, of waiting threads. The mutex can be locked or free. At initialization, the mutex is set to the free state.
#include <chorus.h> int mutexInit(KnMutex* mutex);
As for semaphores, the mutex must have been previously allocated by the user. This implies that mutexes may be allocated where convenient for the application, and that there is no limit imposed by the system on the maximum number of mutexes.
Three operations are provided on these mutexes.
mutexGet() acquires the mutex: if the mutex is free, it is atomically locked and the thread continues its execution.
#include <chorus.h> int mutexGet(KnMutex* mutex);
If the mutex is locked when the mutexGet() operation is invoked, the thread is blocked and queued in the list of threads, waiting for the mutex to become free. Note that there is no way to limit the time during which a thread waits to acquire a mutex.
mutexRel() releases the mutex, returning it to its free state. If threads are blocked while waiting for the mutex, one of them is picked up from the list and activated with the mutex locked.
#include <chorus.h> int mutexRel(KnMutex* mutex);
The last operation is similar to mutexGet(), but does not block if the mutex is already locked.
#include <chorus.h> int mutexTry(KnMutex* mutex);
By checking the return value of mutexTry(), you can determine whether the mutex was free and has been acquired by the current thread, or whether the mutex was already locked, in which case the operation has failed.
The following example shows a small and simple library routine named sampleAdd() which receives two integer arguments and adds them to a global variable one after the other. The code of the previous semaphore example has been modified so that both the main thread and the created thread perform a number of calls to that library. When the job is done, the main thread prints the result and terminates the actor. Refer to the mutexInit(2K) man page.
(file: progov/mutex.c) #include <stdio.h> #include <stdlib.h> #include <chorus.h> #define USER_STACK_SIZE (1024 * sizeof(long)) KnSem sampleSem; KnMutex sampleMutex; long grandTotal; int childCreate(KnPc entry) { KnActorPrivilege actorP; KnDefaultStartInfo_f startInfo; char* userStack; int childLid = -1; int res; startInfo.dsType = K_DEFAULT_START_INFO; startInfo.dsSystemStackSize = K_DEFAULT_STACK_SIZE; res = actorPrivilege(K_MYACTOR, &actorP, NULL); if (res != K_OK) { printf("Cannot get the privilege of the actor, error %d\n", res); exit(1); } if (actorP == K_SUPACTOR) { startInfo.dsPrivilege = K_SUPTHREAD; } else { startInfo.dsPrivilege = K_USERTHREAD; } if (actorP != K_SUPACTOR) { userStack = malloc(USER_STACK_SIZE); if (userStack == NULL) { printf("Cannot allocate user stack\n"); exit(1); } startInfo.dsUserStackPointer = userStack + USER_STACK_SIZE; } startInfo.dsEntry = entry; res = threadCreate(K_MYACTOR, &childLid, K_ACTIVE, 0, &startInfo); if (res != K_OK) { printf("Cannot create the thread, error %d\n", res); exit(1); } return childLid; } void sampleAdd(int a, int b) { int res; res = mutexGet(&sampleMutex); grandTotal += a; grandTotal += b; res = mutexRel(&sampleMutex); } void sampleThread() { int res; int i; for(i = 0; i < 10; i++) { sampleAdd(threadSelf(), i); /* Why not ??? */ } res = semV(&sampleSem); if (res != K_OK){ printf("Cannot perform the semV operation, error %d\n", res); exit(1); } /* Suicide */ threadDelete(K_MYACTOR, K_MYSELF); } int main(int argc, char** argv, char**envp) { int i; int newThreadLi; int res; res = semInit(&sampleSem, 0); if (res != K_OK) { printf("Cannot initialize the semaphore, error %d\n", res); exit(1); } res = mutexInit(&sampleMutex); newThreadLi = childCreate((KnPc)sampleThread); for(i = 0; i < 20; i++){ sampleAdd(threadSelf(), i); /* Why not ??? */ } res = semP(&sampleSem, K_NOTIMEOUT); if (res != K_OK) { printf("Cannot perform the semP operation, error %d\n", res); exit(1); } printf("grandTotal is %d\n", grandTotal); return 0; }
The mutex is allocated within the global data of the actor and is initialized before it is ever used.
The sampleAdd() routine uses the mutex to protect access to the grandTotal variable and make it atomic. Note that the mutexGet() and mutexRel() operations perform the bulk of the work. Mutex operations should always be used in pairs, as in this example.
A mutex is not recursive, a thread which has locked a mutex will deadlock if it tries to perform a second mutexGet() operation on the same mutex.
The ChorusOS operating system provides two alternative ways of scheduling threads. These two features are mutually exclusive:
either the ChorusOS operating system is configured with the default scheduler,
or it is configured with the ROUND_ROBIN
feature.
The default FIFO scheduler defines a pure priority-based, preemptive, FIFO (first-in first-out) policy. Priority of threads may vary from K_FIFO_PRIOMAX (0 and highest priority) to K_FIFO_PRIOMIN (255 and lowest priority). Within this policy, a thread which becomes ready to run after being blocked is always inserted at the end of its priority ready queue. A running thread is preempted only if a thread of a strictly higher priority becomes ready to run. A preempted thread is placed at the head of its priority queue, so that it will be selected when no other ready thread has a greater priority.
The ROUND_ROBIN
feature is a general framework
supporting simultaneous multiple scheduling policies or classes. The main
classes dealt with here are the CLASS_FIFO
and the CLASS_RR
policies.
The CLASS_FIFO
reproduces the behavior of the
default scheduler policy precisely.
The CLASS_RR
implements a priority-based preemptive
policy with round-robin time slicing. Priority of threads may vary from K_RR_PRIOMAX to K_RR_PRIOMIN. It is similar to
the default scheduler policy, except that an elected thread is given a fixed
time quantum. If the thread is still running at quantum expiration, it is
de-scheduled and placed at the end of its priority queue, thus yielding the
processor to other threads of equal priority (if any).
It is possible to set scheduling attributes of threads at thread creation time (using the void* schedParams parameter of threadCreate()). It is also possible to get and modify scheduling attributes of a thread dynamically through the following call.
#include <chorus.h> #include <sched/chFifo.h> #include <sched/chRr.h> #include <sched/chRt.h> #include <sched/chTs.h> int threadScheduler(KnCap* actorCap, KnThreadLid thLi, void* oldParam, void* newParam);
This service enables you to get or set scheduling parameters of any thread of any actor, as long as both the actor capability and the thread identifier are known. threadScheduler() returns the current scheduling attributes of the target thread at the location defined by oldParam, if non-null. It will also set the attributes of the target thread according to the description provided at the location defined by newParam if non-null.
As the size, layout and semantics of scheduling parameters may vary
depending on the scheduler configured in the system, or on the class of the ROUND_ROBIN
framework, parameters are untyped in the generic
interface definition. However, all scheduling parameter descriptions are similar,
at least for the initial fields:
struct KnFifoThParms { KnSchedClass fifoClass; /* Always set to K_SCHED_FIFO */ KnFifoPriority fifoPriority; } KnFifoThParms; struct KnRrThParms { KnSchedClass rrClass; /* Always set to K_SCHED_RR */ KnRrPriority rrPriority; } KnRrThParms;
The first field defines the scheduling policy applied or to be applied to the thread. The second field defines the priority of the thread within the scheduling policy.
Example 6-5 is based on the semaphore example, with a modification to the childCreate() routine so that it can receive scheduling attributes of the thread to be created. The main thread invokes this modified routine, so that the created thread will start as soon as it is created, rather than waiting for the main thread to yield the processor. Thus, the created thread must be given a higher priority than the main thread.
Refer to the threadScheduler(2K) and threadCreate(2K) man pages.
(file: progov/thSched.c) #include <stdio.h> #include <stdlib.h> #include <chorus.h> #define USER_STACK_SIZE (1024 * sizeof(long)) KnSem sampleSem; int childSchedCreate(KnPc entry, void* schedParams) { KnActorPrivilege actorP; KnDefaultStartInfo_f startInfo; char* userStack; int childLid = -1; int res; /* Set defaults startInfo fields */ startInfo.dsType = K_DEFAULT_START_INFO; startInfo.dsSystemStackSize = K_DEFAULT_STACK_SIZE; /* Get actor's privilege */ res = actorPrivilege(K_MYACTOR, &actorP, NULL); if (res != K_OK) { printf("Cannot get the privilege of the actor, error %d\n", res); exit(1); } /* Set thread privilege */ if (actorP == K_SUPACTOR) { startInfo.dsPrivilege = K_SUPTHREAD; } else { startInfo.dsPrivilege = K_USERTHREAD; } /* Allocate a stack for user threads */ if (actorP != K_SUPACTOR) { userStack = malloc(USER_STACK_SIZE); if (userStack == NULL) { printf("Cannot allocate user stack\n"); exit(1); } startInfo.dsUserStackPointer = userStack + USER_STACK_SIZE; } /* Set entry point for the new thread */ startInfo.dsEntry = entry; /* Create the thread in the active state */ res = threadCreate(K_MYACTOR, &childLid, K_ACTIVE, schedParams, &startInfo); if (res != K_OK) { printf("Cannot create the thread, error %d\n", res); exit(1); } return childLid; } void sampleThread() { int myThreadLi; int res; myThreadLi = threadSelf(); printf("I am the new thread. My thread identifier is: %d\n", myThreadLi); res = semV(&sampleSem); if (res != K_OK){ printf("Cannot perform the semV operation, error %d\n", res); exit(1); } threadDelete(K_MYACTOR, K_MYSELF); } int main(int argc, char** argv, char**envp) { int myThreadLi; int newThreadLi; int res; KnThreadDefaultSched schedParams; res = semInit(&sampleSem, 0); if (res != K_OK) { printf("Cannot initialize the semaphore, error %d\n", res); exit(1); } /* acquire my own scheduling attributes */ res = threadScheduler(K_MYACTOR, K_MYSELF, &schedParams, NULL); /* Increase priority of thread to be created */ schedParams.tdPriority -= 1; newThreadLi = childSchedCreate((KnPc)sampleThread, &schedParams); myThreadLi = threadSelf(); printf("Parent thread identifier = %d, Child thread identifier = %d\n", myThreadLi, newThreadLi); res = semP(&sampleSem, K_NOTIMEOUT); if (res != K_OK) { printf("Cannot perform the semP operation, error %d\n", res); exit(1); } return 0; }
First, the main thread needs to get its own scheduling attributes. As these are not known, a KnThreadDefaultSched structure is used as the output argument of the call to threadScheduler(). The last argument of threadScheduler() is set to null as the current scheduling attributes of the main thread wish to be preserved.
In order to give a higher priority to the created thread, decrease the numerical value of the priority. Increasing the priority value has the reverse effect.
One of the most common issues in a multithreaded environment is how to manage per-thread data structures. This may become an important question for libraries. In a single-threaded process, managing these data as global variables is fine. In a multithreaded environment, it will no longer work.
The ChorusOS operating system provides a convenient way for threads to manage per-thread data. A piece of data which needs to be instantiated on a per-thread basis must be associated with a unique key. The key may be obtained from the system through a call to ptdKeyCreate(). This data may accessed using specific calls named ptdSet() and ptdGet().
#include <pd/chPd.h> int ptdKeyCreate(PdKey* key, KnPdHdl destructor);
ptdKeyCreate() generates a unique key, which is opaque to the user. This key is stored at the location defined by the key argument. The user may, optionally, specify a routine as the destructor argument. This routine will be invoked at thread deletion time and will be passed the value associated with key. Upon return from ptdKeyCreate(), the value associated with key is 0. This type of key is visible to all threads of the actor, but each thread using a given key will have its own private copy of the data.
#include <pd/chPd.h> int ptdSet(PdKey key, void* value);
ptdSet() enables a thread to associate the value value with the key key which has been generated previously by a call to ptdKeyCreate().
#include <pd/chPd.h> int ptdGet(PdKey key);
ptdGet() returns the last value associated with the key by this same thread.
Example 6-6 includes a small library that returns a pointer to the next word of a string. This is a simplified version of the strtok() C library routine. For simplicity, it is assumed that words are always separated by spaces in the string.
This library is callable simultaneously from different threads, each thread working on its own string. The routine that returns the pointer to the next word does not take any parameters.
These routines are called snw routines (where snw stands for String Next Word). There is a snwSet(char *str) routine which defines the string that will be looked up by the invoking thread, and a char* snwGet() returning a pointer to the next word.
The library is invoked from the main thread and the created thread on two different strings in order to count the number of words in each string. The results are printed and the threads are synchronized before terminating the actor.
Refer to the ptdKeyCreate(2K), ptdSet(2K), and ptdGet(2K) man pages.
(file: progov/perThreadData.c) #include <stdio.h> #include <string.h> #include <stdlib.h> #include <chorus.h> #include <pd/chPd.h> #define USER_STACK_SIZE (1024 * sizeof(long)) KnSem sampleSem; PdKey snwKey; int childCreate(KnPc entry) { KnActorPrivilege actorP; KnDefaultStartInfo_f startInfo; char* userStack; int childLid = -1; int res; startInfo.dsType = K_DEFAULT_START_INFO; startInfo.dsSystemStackSize = K_DEFAULT_STACK_SIZE; res = actorPrivilege(K_MYACTOR, &actorP, NULL); if (res != K_OK) { printf("Cannot get the privilege of the actor, error %d\n", res); exit(1); } if (actorP == K_SUPACTOR) { startInfo.dsPrivilege = K_SUPTHREAD; } else { startInfo.dsPrivilege = K_USERTHREAD; } if (actorP != K_SUPACTOR) { userStack = malloc(USER_STACK_SIZE); if (userStack == NULL) { printf("Cannot allocate user stack\n"); exit(1); } startInfo.dsUserStackPointer = userStack + USER_STACK_SIZE; } startInfo.dsEntry = entry; res = threadCreate(K_MYACTOR, &childLid, K_ACTIVE, 0, &startInfo); if (res != K_OK) { printf("Cannot create the thread, error %d\n", res); exit(1); } return childLid; } void snwInit() { int res; /* Just allocate a key for our "snw" library */ res = ptdKeyCreate(&snwKey, NULL); if (res != K_OK) { printf("Cannot create a ptd key, error %d\n", res); exit(1); } } void snwSet(char* str) { int res; res = ptdSet(snwKey, str); if (res != K_OK) { printf("Cannot set the ptd key, error %d\n", res); exit(1); } } char* snwGet() { int res; char* p; char* s; p = (char*)ptdGet(snwKey); if (p == NULL) return NULL; s = strchr(p, ' '); if (s != NULL) { s++; } else if (*p != '\0') { /* Last word might not have a following space */ s = p + strlen(p); } res = ptdSet(snwKey, s); return s; } void sampleThread() { char* ptr; int words = 0; int res; snwSet("This is the child thread!"); for (ptr= snwGet(); ptr != NULL; ptr = snwGet()) { words++; } printf("Child thread found %d words.\n", words); res = semV(&sampleSem); if (res != K_OK){ printf("Cannot perform the semV operation, error %d\n", res); exit(1); } threadDelete(K_MYACTOR, K_MYSELF); } int main(int argc, char** argv, char**envp) { char* ptr; int words = 0; int res; int newThreadLi; res = semInit(&sampleSem, 0); if (res != K_OK) { printf("Cannot initialize the semaphore, error %d\n", res); exit(1); } snwInit(); newThreadLi = childCreate((KnPc)sampleThread); snwSet("I am the main thread and counting words in this string!"); for (ptr= snwGet(); ptr != NULL; ptr = snwGet()) { words++; } printf("Main thread found %d words.\n", words); res = semP(&sampleSem, K_NOTIMEOUT); if (res != K_OK) { printf("Cannot perform the semP operation, error %d\n", res); exit(1); } return 0; }
As illustrated in the previous example, it is often the case that C and C++ libraries have been designed for UNIX processes which were initially mono-threaded entities. In order to allow C programmers to continue using the usual libraries within multithreaded actors, the ChorusOS operating environment provides a set of adapted C libraries which may be used from different threads of a given actor without encountering problems.
In the previous examples, some of these adapted libraries, such as printf(), fprintf(), fopen(), and malloc(), were already used. All of these C libraries have been adapted to work efficiently even within a multithreaded actor. Modifications are not visible to the programmer. They rely mainly on synchronization such as mutexes for protecting critical sections and on the per-thread data mechanism to store per-thread global data.
Some libraries did not require any modification and can work in a straightforward fashion within a multithreaded actor. These libraries, such as strtol() (string to lower case), work exclusively on local variables and do not access or generate any global states.