Document: N1287
Date: 2008-03-12
Author: P.J. Plauger – Dinkumware, Ltd.

Dinkum Threads Library


A C or C++ program can call on a number of functions from the Dinkum Threads Library, a portable library for managing multiple threads of control. The next C++ Standard will contain a threads library highly compatible with the C++ portion of the Dinkum Threads Library. We therefore propose that the C portion described here be added to the next C Standard, as field proven technology that is also known to be compatible with the upcoming C++ Standard.


Table of Contents

Overview · Memory Visibility · Condition Variables · Mutexes · Once Functions · Thread-specific Storage · Checked and Unchecked Libraries · C Interface · "Dinkum/threads/threads.h" · "Dinkum/threads/xtimec.h"


Overview

A thread is a separate flow of execution within an application. On a multi-processor system threads can execute simultaneously on different processors. On a single-processor system and on a multi-processor system with fewer available processors than active threads two or more threads must share a processor. The details of switching a processor from one thread to another are handled by the operating system.

The Dinkum Threads Library lets you create and control multiple threads, and synchronize the sharing of data between these threads. It consists of compatible and complementary interfaces for programming in either C or C++. The C Interface is very similar to the thread support interface defined in the the Posix Standard (also known as pthreads), while the C++ Interface is very similar to the boost.threads library for C++.

When a C or C++ program begins execution it runs in a single thread, executing the main function. The program can create additional threads as needed. Each thread has its own copy of all auto variables, so auto values in one thread are independent of auto values in the other threads. Data with static storage duration is accessible to all threads, so those values are shared. However, changes to values in shared data often are not immediately visible in other threads. A multi-threaded application uses condition variables, mutexes, and once functions to coordinate the use of shared data by its threads, in order to ensure that shared data is not made inconsistent by simultaneous changes from more than one thread, that changes to shared data are visible to a thread when needed, and that a thread that needs data that is being created by another thread can be notified when that data becomes available.

Memory Visibility

Changes made by one thread to values in shared data often are not immediately visible in other threads. For example, on a system with two separate processors and two threads running on the two processors, if the two processors simply share main memory an attempt by one processor to write data while the other processor reads the same data could result in the second processor reading a data value that has only been partially changed by the other processor. In order to avoid this inconsistency one processor has to lock the other one out until it finishes. This locking is usually done through the hardware and is known as a bus lock. Bus locks are unavoidable, but they slow the processors down. To minimize the effect of this slowdown, multi-processor systems have separate cache memory for each processor. This cache memory holds a copy of some of the data in main memory. When a processor writes data it writes to the cache. Sometime later the changes made to the cache are written to main memory. Thus, the processors could each have a different value for a data item in their caches, and those values could be different from the value in main memory.

There are three times at which changes made to memory by one thread are guaranteed to be visible in another thread:

In practice this means that:

Note, however, that locking a mutex to prevent modification of shared data while it is being read also prevents other threads from locking the mutex in order to read the data. Such critical sections should be kept as short as possible to avoid blocking other threads any longer than necessary.

Condition Variables

A condition variable is used by a thread to wait until another thread notifies it that a condition has become true. Code that waits for a condition variable must also use a mutex; before calling any of the functions that wait for the condition variable the calling thread must lock the mutex, and when the called function returns the mutex will be locked. During the time that a thread is blocked waiting for the condition to become true the mutex is not locked.

Spurious wakeups occur when threads waiting for condition variables become unblocked without appropriate notifications. Code that waits for a condition to become true should explicitly check that condition when returning from a wait function to recognize such spurious wakeups. This is usually done with a loop:

while (condition is false)
    wait for condition variable;

The condition variable functions use a mutex internally; when a thread returns from a wait function any changes made to memory by threads that called a wait function or a notify function before the return will be visible to the caller.

Mutexes

A mutex is used to insure that only one thread executes a region of code, known as a critical section, at any one time. On entry into the critical section the code locks the mutex; if no other thread holds the mutex the lock operation succeeds and the calling thread holds the mutex. On exit from the critical section the code unlocks the mutex. If another thread holds the mutex when a thread tries to lock it the thread that tried to lock the mutex blocks until the mutex is unlocked. When more than one thread is blocked waiting for the mutex an unlock releases one of the blocked threads.

A mutex can be recursive or non-recursive. When a thread that already holds a recursive mutex attempts to lock it again the thread does not block. The thread must unlock the mutex as many times as it locked it before any other thread will be permitted to lock the mutex. When a thread that already holds a non-recursive mutex attempts to lock it again the thread will block. Since the thread cannot then unlock the mutex, the result is a deadlock. Non-recursive mutexes are usually smaller and faster than recursive mutexes, so a properly written program that uses non-recursive mutexes can be faster than one that uses recursive mutexes.

A mutex supports test and return if it provides a lock call that does not block if the mutex is already locked. Such a lock call returns a value that indicates whether the mutex was locked as a result of the call.

A mutex supports timeout if it provides a lock call that blocks until no later than a specified time waiting for the mutex to be unlocked. Such a lock call returns a value that indicates whether the mutex was locked as a result of the call.

Once Functions

A once function is a function that should only be called once during a program's execution. Once functions are typically used to initialize data that is shared between threads: the first thread that needs the data initializes it by calling the once function, and later threads that need the data do not call the once function. Each once function should have an associated once flag, statically initialized to indicate that the function has not been called. Code that needs to insure that the once function has been called calls call_once, passing the flag and the address of the once function. The code in call_once atomically checks the flag, and if the flag indicates that the function has not been called, calls the once function and sets the flag to indicate that the function has been called.

The function call_once uses a mutex internally; when it returns any changes made to memory by the once function will be visible to the caller.

Thread-specific Storage

Thread-specific storage is global data that can hold a distinct value for each thread that uses it. This permits functions executing in a single thread to share data without interfering with the data shared by the same functions when executing in other threads.


C Interface


The C interface to the Dinkum Threads Library is very similar to the thread support interface defined in the Posix Standard. It consists of two headers:

Here is an example program to illustrate the use of the C interface.


Threads · Condition Variables · Mutexes · Once Functions · Thread-specific Storage · Return Values


Threads

Use the functions and types with the prefix thrd to manage threads. Each thread has an identifier of type thrd_t, which is passed as an argument to the functions that manage specific threads. Each thread begins execution in a function of type thrd_start_t. To create a new thread call the function thrd_create with the address of the thread identifier, the address of the thread function, and an argument to be passed to the thread function. The thread ends when it returns from the thread function or when it calls thrd_exit. For convenience, a thread can provide a result code of type int when it ends, either by returning the code from the thread function or by passing the code to thrd_exit. To block a thread until another thread ends call thrd_join, passing the identifier of the thread to wait for and, optionally, the address of a variable of type int where the result code will be stored. To properly clean up resources allocated by the operating system, an application should call either thrd_join or thrd_detach once for each thread created by thrd_create.

Two functions operate on the current thread; they do not take a thread identifier argument. Use thrd_sleep to suspend execution of the current thread until a particular time. Use thrd_yield to permit other threads to run even if the current thread would ordinarily continue to run.

Two functions operate on thread identifiers. Use thrd_equal to determine whether two thread identifiers refer to the same thread. Use thrd_current to get a thread identifier that refers to the current thread.

Condition Variables

Use the functions and type with the prefix cnd to manage condition variables. Each condition variable has an identifier of type cnd_t, which is passed as an argument to the functions that manage condition variables. Use cnd_init to create a condition variable and cnd_destroy to release any resources associated with a condition variable when it is no longer needed. To wait for a condition variable to be signalled call cnd_wait or cnd_timedwait. To unblock threads waiting for a condition variable call cnd_signal or cnd_broadcast.

Mutexes

Use the functions and type with the prefix mtx to manage mutexes. Each mutex has an identifier of type mtx_t, which is passed as an argument to the functions that manage mutexes. Use mtx_init to create a mutex and mtx_destroy to release any resources associated with a mutex when it is no longer needed. To lock a mutex call mtx_lock, mtx_timedlock or mtx_trylock. To unlock a mutex call mtx_unlock.

Once Functions

Use a value of type once_flag, initialized to the value ONCE_FLAG_INIT, to ensure that a function is called exactly once by passing a function pointer and the address of the once_flag object to call_once.

Thread-specific Storage

Use the functions and types with the prefix tss to manage thread-specific storage. Each thread-specific storage pointer has an identifier of type tss_t, which is passed as an argument to the functions that manage thread-specific storage. Call tss_create to create a thread-specific storage pointer and tss_delete to release any resources associated with a thread-specific storage pointer when it is no longer needed. To get the value held by the pointer in the current thread call tss_get. To change the value held by the pointer in the current thread call tss_set.

Each thread-specific storage pointer may have an associated destructor, specified in the call to tss_create. The destructor will be called when a thread terminates and the value of the pointer associated with that thread is not 0. The value of the pointer for that thread is set to 0 before calling the destructor and the old value is passed to the destructor. Since a destructor can store non-0 values in thread-specific storage pointers, this process will be repeated until no pointers for the terminating thread hold non-0 values or until a system-specific maximum number of iterations TSS_DTOR_ITERATIONS has been made.

Return Values

Most of the functions return a value of type int that indicates whether the function succeeded. The values are as follows:


"Dinkum/threads/threads.h"


call_once · cnd_broadcast · cnd_destroy · cnd_init · cnd_signal · cnd_t · cnd_timedwait · cnd_wait · mtx_destroy · mtx_init · mtx_lock · mtx_plain · mtx_recursive · mtx_t · mtx_timed · mtx_timedlock · mtx_try · mtx_trylock · mtx_unlock · once_flag · ONCE_FLAG_INIT · thrd_abort · thrd_busy · thrd_create · thrd_current · thrd_detach · thrd_equal · thrd_error · thrd_exit · thrd_join · thrd_nomem · thrd_sleep · thrd_start_t · thrd_success · thrd_t · thrd_timedout · thrd_yield · tss_create · TSS_DTOR_ITERATIONS · tss_delete · tss_dtor_t · tss_get · tss_set · tss_t


Include the header "Dinkum/threads/threads.h" so that you can perform thread-related operations.

    /* RETURN VALUES */
enum {
    thrd_success = .....,
    thrd_nomem = .....,
    thrd_timedout = .....,
    thrd_busy = .....,
    thrd_error = .....
    };

    /* THREADS */
typedef o-type thrd_t;
typedef int (*thrd_start_t)(void*);

int thrd_create(thrd_t *, thrd_start_t, void*);
int thrd_detach(thrd_t);
void thrd_exit(int);
int thrd_join(thrd_t, int*);
void thrd_sleep(const xtime*);
void thrd_yield(void);

int thrd_equal(thrd_t, thrd_t);
thrd_t thrd_current(void);

    /* MUTEXES */
typedef o-type mtx_t;
enum {
    mtx_plain = .....,
    mtx_try = .....,
    mtx_timed = .....,
    mtx_recursive = .....
    };

int mtx_init(mtx_t*, int);
void mtx_destroy(mtx_t*);
int mtx_lock(mtx_t*);
int mtx_trylock(mtx_t*);
int mtx_timedlock(mtx_t*, const xtime*);
int mtx_unlock(mtx_t*);

    /* CONDITION VARIABLES */
typedef o_type cnd_t ;

int cnd_init(cnd_t*);
void cnd_destroy(cnd_t*);
int cnd_wait(cnd_t*, mtx_t*);
int cnd_timedwait(cnd_t*, mtx_t*, const xtime*);
int cnd_signal(cnd_t*);
int cnd_broadcast(cnd_t*);

    /* THREAD-SPECIFIC STORAGE */
typedef i-type tss_t;
typedef void (*tss_dtor_t)(void*);

int tss_create(tss_t*, tss_dtor_t);
int tss_delete(tss_t);
int tss_set(tss_t, void*);
void *tss_get(tss_t);
#define TSS_DTOR_ITERATIONS <integer constant expression>

    /* ONCE FUNCTIONS */
typedef o-type once_flag;

#define ONCE_FLAG_INIT <object initializer>
void call_once(once_flag*, void (*)(void));

    /* UTILITY FUNCTIONS */
void thrd_abort(const char *);

call_once

void call_once(once_flag *flag, void (*func)(void));

The function uses *flag to ensure that func is called exactly once.

cnd_broadcast

int cnd_broadcast(cnd_t *cond);

Returns: the usual return value.

The function unblocks all of the threads that are blocked on the condition variable *cond at the time of the call. If no threads are blocked on the condition variable at the time of the call the function does nothing.

cnd_destroy

void cnd_destroy(cnd_t *cond);

Precondition: no threads are blocked waiting for *cond.

The function releases any resources used by the condition variable *cond.

cnd_init

int cnd_init(cnd_t *cond);

Returns: the usual return value.

The function creates a condition variable. If it succeeds it sets *cond to a value that uniquely identifies the newly created condition variable. A thread that calls cnd_wait on a newly created condition variable will block.

cnd_signal

int cnd_signal(cnd_t *cond);

Returns: the usual return value.

The function unblocks one of the threads that is blocked on the condition variable *cond at the time of the call. If no threads are blocked on the condition variable at the time of the call the function does nothing.

cnd_t

typedef o-type cnd_t;

The type is an object type o-type that holds an identifier for a condition variable.

cnd_timedwait

int cnd_timedwait(cnd_t *cond, mtx_t *mtx, const xtime *xt);

Requires: the mutex *mtx must be locked by the calling thread

Returns: the usual return value.

The function atomically unlocks the mutex mtx and blocks until the condition variable *cond is signaled by a call to cnd_signal or to cnd_broadcast, or until after the time specified by the xtime object *xt. When the calling thread becomes unblocked it locks *mtx before it returns.

cnd_wait

int cnd_wait(cnd_t *cond, mtx_t *mtx);

Requires: the mutex *mtx must be locked by the calling thread

Returns: the usual return value.

The function atomically unlocks the mutex and blocks until the condition variable *cond is signaled by a call to cnd_signal or to cnd_broadcast. When the calling thread becomes unblocked it locks *mtx before it returns.

mtx_destroy

void mtx_destroy(mtx_t *mtx);

Precondition: no threads are blocked waiting for *mtx.

The function releases any resources used by the mutex *mtx.

mtx_init

int mtx_init(mtx_t *mtx, int type);

Returns: the usual return value.

The function creates a mutex object with properties indicated by type, which must have one of the six values

If it succeeds it sets *mtx to a value that uniquely identifies the newly created mutex.

mtx_lock

int mtx_lock(mtx_t *mtx);

Precondition: if the mutex is non-recursive it must not be locked by the calling thread.

Returns: the usual return value.

The function blocks until it locks the mutex *mtx.

mtx_plain

enum { mtx_plain = ..... };

The compile-time constant is passed to mtx_init to create a mutex object that supports neither timeout nor test and return.

mtx_recursive

enum { mtx_recursive = ..... };

The compile-time constant is passed to mtx_init to create a mutex object that supports recursive locking.

mtx_t

typedef o-type mtx_t;

The type is an object type o-type that holds an identifier for a mutex.

mtx_timed

enum { mtx_timed = ..... };

The compile-time constant is passed to mtx_init to create a mutex object that supports timeout.

mtx_timedlock

int mtx_timedlock(mtx_t *mtx, const xtime *xt);

Precondition: the mutex *mtx must be of type mtx_timed or of type mtx_timed | mtx_recursive.

Returns: the usual return value.

The function blocks until it locks the mutex *mtx or until the time specified by the xtime object *xt.

mtx_try

enum { mtx_try = ..... };

The compile-time constant is passed to mtx_init to create a mutex object that supports test and return.

mtx_trylock

int mtx_trylock(mtx_t *mtx);

Precondition: the mutex *mtx must be of type mtx_try, of type mtx_try | mtx_recursive, of type mtx_timed, or of type mtx_timed | mtx_recursive.

Returns: the usual return value.

The function attempts to lock the mutex *mtx. If the mutex is already locked the function returns without blocking.

mtx_unlock

int mtx_unlock(mtx_t *mtx);

Precondition: the mutex *mtx must be locked by the calling thread.

Returns: the usual return value.

The function unlocks the mutex *mtx.

once_flag

typedef o-type once_flag;

The type is an object type o-type that holds a flag for use by call_once.

ONCE_FLAG_INIT

#define ONCE_FLAG_INIT <object initializer>

The macro yields a value that can be used to initialize an object of type once_flag.

thrd_abort

void thrd_abort(const char *msg);

The function writes msg to standard error then calls abort.

thrd_busy

enum { thrd_busy = ..... };

The compile-time constant is returned by a function to indicate that the requested operation failed because a resource requested by a test and return function is already in use.

thrd_create

int thrd_create(thrd_t *thr, thrd_start_t func, void *arg);

Returns: the usual return value.

The function creates a new thread executing func(arg). If it succeeds it sets *thr to a value that uniquely identifies the newly created thread. The function does not return until the new thread has begun execution.

thrd_current

thrd_t thrd_current(void);

The function returns a value that uniquely identifies the thread that called it.

thrd_detach

int thrd_detach(thrd_t thr);

Requires: the application must not have previously called thrd_detach or thrd_join for the thread identified by thr.

Returns: the usual return value.

The function tells the operating system to dispose of any resources allocated to the thread identified by thr when that thread terminates.

thrd_equal

int thrd_equal(thrd_t thr0, thrd_t thr1);

The function returns zero if thr0 and thr1 refer to different threads. Otherwise it returns a non-zero value.

thrd_error

enum { thrd_error = ..... };

The compile-time constant is returned by a function to indicate that the requested operation failed.

thrd_exit

void thrd_exit(int res);

The function terminates execution of the calling thread and sets its result code to res.

thrd_join

int thrd_join(thrd_t thr, int *res);

Requires: the application must not have previously called thrd_join or thrd_detach for the thread identified by thr.

Returns: the usual return value.

The function tells the operating system to dispose of any resources allocated to the thread identified by thr when that thread terminates and blocks until that thread has terminated. If res is not a null pointer it stores the thread's result code in *res.

thrd_nomem

enum { thrd_nomem = ..... };

The compile-time constant is returned by a function to indicate that the requested operation failed because it was unable to allocate memory.

thrd_sleep

void thrd_sleep(const xtime *xt);

The function suspends execution of the calling thread until after the time specified by the xtime object *xt.

thrd_start_t

typedef int (*thrd_start_t)(void*);

The type is the function type that is passed to thrd_create to create a new thread.

thrd_success

enum { thrd_success = ..... };

The compile-time constant is returned by a function to indicate that the requested operation succeeded.

thrd_t

typedef o-type thrd_t;

The type is an object type o-type that holds an identifier for a thread.

thrd_timedout

enum { thrd_timedout = ..... };

The compile-time constant is returned by a timed wait function to indicate that the time specified in the call was reached without acquiring the requested resource.

thrd_yield

void thrd_yield(void);

The function permits other threads to run even if the current thread would ordinarily continue to run.

tss_create

int tss_create(tss_t *key, tss_dtor_t dtor);

Returns: the usual return value.

The function creates a thread-specific storage pointer with destructor dtor, which may be null. If it succeeds it sets *key to a value that uniquely identifies the newly created pointer.

TSS_DTOR_ITERATIONS

#define TSS_DTOR_ITERATIONS <integer constant expression>

The macro yields the maximum number of times that destructors will be called when a thread terminates.

tss_dtor_t

typedef void (*tss_dtor_t)(void*);

The type is the function type for a destructor for a thread-specific storage pointer.

tss_delete

void tss_delete(tss_t key);

The function releases any resources used by the thread-specific storage pointer key.

tss_get

void *tss_get(tss_t key);

The function returns the value for the current thread held in the thread-specific storage pointer identified by key.

tss_set

int tss_set(tss_t key, void *val);

Returns: the usual return value.

The function sets the value for the current thread held in the thread-specific storage pointer identified by key to val.

tss_t

typedef o-type tss_t;

The type is an object type o-type that holds an identifier for a thread-specific storage pointer.


"Dinkum/threads/xtimec.h"


TIME_UTC · xtime · xtime_get


Include the header "Dinkum/threads/xtimec.h" so that you can perform high-resolution time operations.

    /* TYPES */
typedef o-type xtime;

    /* CONSTANTS */
enum { TIME_UTC = <integer constant expression> };

    /* FUNCTIONS */
int xtime_get(xtime *, int);

TIME_UTC

enum { TIME_UTC = <integer constant expression> };

The compile-time constant is a non-zero value that designates Coordinated Universal Time (UTC) as the time base for the values set by xtime_get.

xtime

typedef struct {
    int sec;    /* seconds since 1 Jan. 1970 00:00:00 */
    int nsec;   /* nanoseconds since time specified by sec */
    } xtime;

The struct xtime contains members that describe times with nanosecond resolution. The comment following each member desribes its meaning.

xtime_get

int xtime_get(xtime *xt, int base);

The function sets the xtime object pointed to by xt to hold the current time based on the time base base. If successful it returns the non-zero value base, which must be TIME_UTC; otherwise it returns 0. Note that although an xtime object describes times with nanosecond resolution the actual resolution in an xtime object is system dependent.

Copyright © 2002-2008 by Dinkumware, Ltd.