ChorusOS 4.0 Introduction

Chapter 6 Multithreaded Programming with the ChorusOS Operating System

This chapter describes how to use ChorusOS operating system services to create a multithreaded actor. It contains the following sections:

Basic Multi-Thread Programming

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.

Figure 6-1 A Multi-Threaded Actor

Graphic

Thread Handling

Getting a Thread Identifier

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.

Creating a Thread

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.


Note -

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:

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:

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.


Example 6-1 Creating a Thread

(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;
}


Deleting a Thread

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.


Note -

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.


Example 6-2 Deleting a Thread

(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;
}


Synchronizing Threads

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

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.

Figure 6-2 Two Threads Synchronizing with a Semaphore

Graphic

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.


Example 6-3 Synchronizing Using Semaphores

(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;
}


Mutexes

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.

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.


Example 6-4 Protecting Shared Data Using Mutexes

(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;
}


Basic Scheduling Control

The ChorusOS operating system provides two alternative ways of scheduling threads. These two features are mutually exclusive:

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.


Example 6-5 Changing Scheduling Attributes

(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;
}


Managing Per-Thread Data

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.


Example 6-6 Managing Per-Thread Data

(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;
}


Threads and Libraries

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.