NASD Programmer's Documentation
Threads

One module provided by the NASD API is the threads module. This presents interfaces for creating and manipulating threads, along with mutex and condition variables. Describing proper techniques for programming with threads, mutexes, and conditions is outside the scope of this document. For further information, the reader is recommended to Birrell's " An Introduction to Programming with Threads."

Basics

To use the NASD threads interface, a file must include <nasd/nasd_threadstuff.h>. To initialize the threads module, call nasd_threads_init(). Like most module initialization functions, this requires no arguments. When the module is no longer needed, call nasd_threads_shutdown(). Note that this shutdown function should only be called after all related resources, such as mutexes, conditions, and thread groups, have been destroyed, and all threads created by this module have exited.

Threads have type nasd_thread_t. A thread is created by specifying a function to run which is that thread, and an argument to pass to this function. This function must be of type void, and the argument of type nasd_threadarg_t, which is guaranteed to be large enough to hold a pointer. To cease execution, this function must call NASD_THREAD_KILL_SELF(), which is the equivalent of returning from this void function. It is not acceptable to simply return from this function. It is also not acceptable to call NASD_THREAD_KILL_SELF() from any function other than that which was directly specified when the thread was created.

Creating threads

The function which creates a thread is:

nasd_status_t nasd_thread_create(nasd_thread_t *threadp,
  nasd_threadfunc_t func, nasd_threadarg_t arg);
where nasd_threadfunc_t is defined as typedef void (*nasd_threadfunc_t)(nasd_threadarg_t);
(That is, a void function taking a nasd_threadarg_t as a single argument.)

When the thread creation function returns, the thread has already been created, and is eligible for scheduling immediately. Indeed, it may be scheduled even before the calling thread receives the return value of the thread creation function, or it may not be dispatched until some later time. To coordinate thread start-up, you may wish to use a thread group.

Thread attributes

Additional attributes may be specified for a newly created thread. Thread attributes are represented by the opaque type nasd_threadattr_t. A single nasd_threadattr_t may be used to create any number of threads with the same attributes. A nasd_threadattr_t may also be destroyed after it has been used to create a thread, before that thread has completed execution. Functions which manipulate thread attribute representations are:

nasd_status_t nasd_threadattr_create(nasd_threadattr_t *attrp);
nasd_status_t nasd_threadattr_destroy(nasd_threadattr_t *attrp);
nasd_status_t nasd_threadattr_setstacksize(nasd_threadattr_t *attrp,
  int size);
nasd_status_t nasd_threadattr_getstacksize(nasd_threadattr_t *attrp,
  int *sizep);
Additionally, there is a separate thread creation function to use when explicitly setting thread attributes: nasd_status_t nasd_thread_create_attr(nasd_thread_t *threadp,
  nasd_threadattr_t *attrp, nasd_threadfunc_t func,
  nasd_threadarg_t arg);
These functions are provided so that users of the NASD API may write multithreaded code which functions across many platforms. Individual platforms may choose to not support any of the thread attribute set and get operations, however, and instead return NASD_OP_NOT_SUPPORTED for these operations. If that value is returned by a get or set operation, it is guaranteed that the contents of the attribute remain unchanged, and the attribute continues to be a valid argument of nasd_thread_create_attr().

Sample code which creates threads exists in the section describing thread groups.

Mutexes

NASD provides simple primitives for mutual exclusion- mutexes with lock and unlock operations. Mutexes in NASD are totally opaque to their users. To declare a mutex:
NASD_DECLARE_MUTEX(mutex_name)
Note that incantations of NASD_DECLARE_MUTEX() are not followed by a semicolon, as one might normally expect. Two other forms of NASD_DECLARE_MUTEX() exist, they are:
NASD_DECLARE_STATIC_MUTEX(mutex_name)
NASD_DECLARE_EXTERN_MUTEX(mutex_name)
The former declares a mutex with the semantics of the C static keyword. That is, it may not be accessed directly by code in other files, but may only be passed by reference. The latter provides a declaration with the semantics of the C extern keyword- no storage is reserved for this symbol, but it may be legally accessed as a mutex, and the linker will look for the storage to be declared in another object. It is legal to NASD_DECLARE_EXTERN_MUTEX() a mutex, and later declare it locally with NASD_DECLARE_MUTEX(). The results of mixing NASD_DECLARE_EXTERN_MUTEX() and NASD_DECLARE_STATIC_MUTEX() are undefined, however.

To create a mutex, call nasd_mutex_init(). This takes a pointer to a mutex as a single argument, and returns a nasd_status_t indicating whether or not the initialization was successful. To destroy a mutex, and free associated resources, call nasd_mutex_destroy() with a pointer to the mutex. This also returns a nasd_status_t indicating success or failure. (Failure usually indicates that either the pointer was not to a valid mutex, or some memory corruption has occurred.)

Two interfaces are provided to lock mutexes: NASD_LOCK_MUTEX() and NASD_TRY_LOCK_MUTEX(). The former will acquire the mutex, blocking if and only if necessary. The latter will never block, but may fail to acquire the mutex. NASD_TRY_LOCK_MUTEX() will evaluate to zero (0). In the case where NASD_TRY_LOCK_MUTEX() fails (evaluates to zero), the state of the mutex remains completely unchanged. In the case where NASD_TRY_LOCK_MUTEX() evaluates non-zero, it is as if NASD_LOCK_MUTEX() just completed. To release a mutex acquired with either NASD_LOCK_MUTEX() or NASD_TRY_LOCK_MUTEX(), use NASD_UNLOCK_MUTEX(). All three of these macros are invoked with the mutex itself, not a reference.

Here is a sample program which illustrates the use of mutexes:

#include <nasd/nasd_options.h>
#include <nasd/nasd_threadstuff.h>

NASD_DECLARE_MUTEX(nasd_sample_mutex)

main()
{
  nasd_status_t rc;

  rc = nasd_threads_init();
  if (rc) {
    fprintf(stderr, "ERROR: failed initializing thread subsystem, rc=0x%x (%s)\n",
      rc, nasd_error_string(rc));
    exit(1);
  }

  rc = nasd_mutex_init(&nasd_sample_mutex);
  if (rc) {
    fprintf(stderr, "ERROR: failed initializing mutex, rc=0x%x (%s)\n",
      rc, nasd_error_string(rc));
    nasd_threads_shutdown();
    exit(1);
  }

  printf("Created the mutex. No one holds it.\n");

  NASD_LOCK_MUTEX(nasd_sample_mutex);
  printf("Holding the mutex\n");
  NASD_UNLOCK_MUTEX(nasd_sample_mutex);

  printf("Not holding the mutex\n");

  if (NASD_TRY_LOCK_MUTEX(nasd_sample_mutex)) {
    printf("Try-lock successful, holding the mutex\n");
    NASD_UNLOCK_MUTEX(nasd_sample_mutex);
  }

  printf("Not holding the mutex\n");

  rc = nasd_mutex_destroy(&nasd_sample_mutex);
  if (rc) {
    /* This is bad news. */
    fprintf(stderr, "ERROR: failed destroying mutex, rc=0x%x (%s)\n",
      rc, nasd_error_string(rc));
    nasd_threads_shutdown();
    exit(1);
  }

  nasd_threads_shutdown();

  exit(0);

}

Condition variables

NASD also provides condition variables with a few simple operations. Like mutexes, these types are totally opaque. They may be declared with: NASD_DECLARE_COND(cond_name)
NASD_DECLARE_STATIC_COND(cond_name)
NASD_DECLARE_EXTERN_COND(cond_name)
These declarators have semantics identical to the equivalent mutex declarators. Likewise, conditions are initialized and destroyed with nasd_cond_init() and nasd_cond_destroy(), each of which takes a pointer to a condition variable as its sole argument, and returns a nasd_status_t indicating the result of this operation.

A thread may block on a condition by using the NASD_WAIT_COND() interface. This takes two arguments: a condition variable and a mutex. Both must be validly initialized, and the caller must hold the mutex. NASD_WAIT_COND() will atomically release the mutex and wait for an event to be indicated on the condition variable. When the condition is raised, NASD_WAIT_COND() will reacquire the mutex automatically before returning. A call to NASD_WAIT_COND() would look something like:

NASD_WAIT_COND(cond_name,mutex_name)

assuming the following declarations:
NASD_DECLARE_MUTEX(mutex_name)
NASD_DECLARE_COND(cond_name)

There are two operations for raising a condition (signalling an event): NASD_SIGNAL_COND() and NASD_BROADCAST_COND(). Each takes as its sole argument a validly-initialized condition. For instance, NASD_BROADCAST_COND(cond_name) could be used to awaken the caller of NASD_WAIT_COND in the above example. There is no requirement that the caller of NASD_BROADCAST_COND() (or NASD_SIGNAL_COND()) hold or not hold any mutexes associated with any calls to NASD_WAIT_COND() made on the condition variable. NASD_SIGNAL_COND() is guaranteed to awaken at least one current caller of NASD_WAIT_COND() (on the indicated condition variable), if there are any. NASD_BROADCAST_COND() is guaranteed to awaken all current callers of NASD_WAIT_COND() on the indicated condition variable. A current caller of NASD_WAIT_COND() is a calling thread which is blocked in a call to NASD_WAIT_COND() at the time NASD_SIGNAL_COND() or NASD_BROADCAST_COND() is called by another thread.

Readers/writers locks

In addition to mutexes, NASD also provides readers/writers locks. These locks provide more complex semantics than mutexes. At any given time, an arbitrary number of threads may hold "read" locks, or exactly one thread may hold a "write" lock. This is intended to provide protection of state shared between many threads, and allow many threads to examine the state without modifying it concurrently, but allow a single thread to modify the state without confusing these readers.

A readers/writers lock is declared with:

NASD_DECLARE_RWLOCK(rwlock_name)
To initialize a readers/writers lock, call nasd_rwlock_init() with a pointer to the lock to be initialized. To destroy it, call nasd_rwlock_destroy() with a pointer to the lock to be destroyed. Both of these functions return type nasd_status_t.

To take a readers/writers lock, call either NASD_LOCK_READ() or NASD_LOCK_WRITE(), passing a readers/writers lock as the sole argument. After NASD_LOCK_READ() completes, there may be any number of other threads also holding "read" locks on this readers/writers lock, but there will be no threads holding write locks. After NASD_LOCK_WRITE() completes, there will be no other threads holding any locks on this readers/writers lock. To release a lock after calling NASD_LOCK_READ(), call NASD_UNLOCK_READ(). To release a lock after calling NASD_LOCK_WRITE(), call NASD_UNLOCK_WRITE().

Important: although mutexes and readers/writers locks are in many ways similar, it is never legal to pass a readers/writers lock as an argument to an operation which expects a mutex, or vice-versa.

Notes on the threads API

The NASD threads module does not guarantee that there will not be spurious wakeups of callers of NASD_WAIT_COND(). Callers should always reverify that the event which they were awaiting has actually occurred. Note also that NASD_SIGNAL_COND() may generate extraneous wakeups of multiple threads.

The NASD threads module is built atop a porting layer, below which exists platform- and system-specific code. For that reason, those operations which return type nasd_status_t may return any code which the particular port of the NASD code you are using deems fit. This may include error codes which are specific to the system or platform you are running on. However, these error codes should always be translatable to meaningful text with nasd_error_string().

No guarantee of wakeup ordering is made for threads waiting for locks or conditions.


<--- ---> ^<br>|<br>|
Modules Thread groups NASD Programmer's Documentation