NASD Programmer's Documentation
Thread Groups

One common problem when programming with threads is coordinating startup and shutdown of threads. Often, the creators of threads do not wish to proceed until they are sure that the created thread has been scheduled at least once, and thus had a chance to perform any necessary local initialization before dispatching its workload. Likewise, when shutting down a system, allocators of resources often wish to be sure that worker threads are no longer using resources before deallocating them. To simplify these activities, NASD provides an abstraction called a thread group. This is a structure used to control and coordinate startup and shutdown of one or more threads.

Thread groups have type nasd_threadgroup_t. They are intialized with nasd_init_threadgroup() and cleaned up with nasd_destroy_threadgroup(), both of which return nasd_status_t.

All thread group operations take as their sole argument a pointer to a nasd_threadgroup_t. To utilize a thread group, it must first be initialized (nasd_init_threadgroup()). When a thread is created, the creator of the thread should call NASD_THREADGROUP_STARTED() on the associated group. When it is ready to perform its work, that thread should call NASD_THREADGROUP_RUNNING(). When it is no longer dispatching its workload, and will never again touch any resources which it might be expected to use during the course of its usual work, that thread should call NASD_THREADGROUP_DONE(). Normally, this takes place immediately before the thread in question ceases execution (NASD_THREAD_KILL_SELF()). The creator of the thread calls NASD_THREADGROUP_WAIT_START() to wait for all threads to be ready to dispatch their workload. To wait for all threads in a group to be done dispatching their workload (that is, to wait for them all to call NASD_THREADGROUP_DONE()), NASD_THREADGROUP_WAIT_STOP() should be called.

It is also useful to have a consistent way for threads to indicate to one another that they should stop dispatching their workload and exit, or for worker threads to check if they should do so. To instruct threads in a group to cease dispatching their workload, a thread should call NASD_THREADGROUP_INDICATE_SHUTDOWN() on the group. When NASD_THREADGROUP_SHUTDOWNP() evaluates nonzero on a group, someone has called NASD_THREADGROUP_INDICATE_SHUTDOWN() on the group (it evaluates zero if no one has done so). This mechanism relies on the threads involved explicitly informing one another that this condition has changed. For instance, a worker thread which alternates blocking to await an event to dispatch and dispatching the event should check NASD_THREADGROUP_SHUTDOWNP() before and after blocking.

The following fragments of code illustrate how one uses a thread group to coordinate starting and stopping a group of threads, and how threads in that group coordinate. Although this example creates multiple worker threads, it is perfectly appropriate to use a thread group to manage the starting and stopping of a single thread.

Starting threads
nasd_thread_t threads[NTHREADS];
nasd_threadgroup_t group;
nasd_status_t rc;
int i;

rc = nasd_init_threadgroup(&group);
if (rc) {
  fprintf(stderr, "ERROR: cannot init thread group rc=0x%x (%s)\n",
    rc, nasd_error_string(rc));
  return(rc);
}

for(i=0;i<NTHREADS;i++) {
  rc = nasd_thread_create(&threads[i],
    worker_func, (nasd_threadarg_t)&group);
  if (rc) {
    fprintf(stderr, "WARNING: failed creating thread #%d rc=0x%x (%s)\n",
       i, rc, nasd_error_string(rc));
    break;
  }

  NASD_THREADGROUP_STARTED(&group);
}

NASD_THREADGROUP_WAIT_START(&group);
return(NASD_SUCCESS);

Stopping threads
/*
 * Set flag indicating that worker threads should finish up and exit.
 * Take the lock to coordinate with the wait on the condition variable
 * in our example worker thread below.
 */
NASD_LOCK_MUTEX(work_list_mutex);
NASD_THREADGROUP_INDICATE_SHUTDOWN(&group);
NASD_UNLOCK_MUTEX(work_list_mutex);

/* Make worker threads notice this flag if they're blocked */
NASD_BROADCAST_COND(work_ready_condition);

/* Wait for the worker threads to finish */
NASD_THREADGROUP_WAIT_STOP(&group);

rc = nasd_destroy_threadgroup(&group);
if (rc) {
  /* Bad day */
  fprintf(stderr, "ERROR: got 0x%x (%s) destroying thread group\n",
    rc, nasd_error_string(rc));
  exit(1);
}

Worker thread
void
worker_thread(
  nasd_threadarg_t  arg)
{
  nasd_threadgroup_t &group_ptr;

  /* retrieve a pointer to our associated thread group */
  group_ptr = (nasd_threadgroup_t *)arg;

  /* let everyone know we're ready to go */
  NASD_THREADGROUP_RUNNING(group_ptr);

  /* grab lock required to examine pending-work list */
  NASD_LOCK_MUTEX(work_list_mutex);

  /*
   * After waking up from above lock operation, or
   * completing work below, re-check to see if we
   * should stop running.
   */
  while(!NASD_THREADGROUP_SHUTDOWNP(group_ptr)) {

    if (work_is_to_be_done() == 0) {
      /* Wait for something to do */
      NASD_WAIT_COND(work_ready_condition, work_list_mutex);
    }

    if (NASD_THREADGROUP_SHUTDOWNP(group_ptr))
      break;

    /*
     * Our mythical function here dequeues and
     * dispatches an event if and only if there
     * is one such pending.
     */
    check_if_work_and_do();
  }

  NASD_UNLOCK_MUTEX(work_list_mutex);

  NASD_THREADGROUP_DONE(group_ptr);
  NASD_THREAD_KILL_SELF();
}


<--- ---> ^<br>|<br>|
Threads Memory NASD Programmer's Documentation