The subroutines provided in the threads library can be used as primitives to build more complex synchronization objects. This article provides implementation examples of some traditional synchronization objects:
The mutexes provided by the threads library are low-contention objects and should not be held for a very long time. Long locks are implemented with mutexes and condition variables, so that a long lock may be held for long time without affecting the performance of the program.
The following implementation is very basic. The lock owner is not checked, any thread can unlock any lock. Error handling and cancellation handling are not performed. As written hereafter, long locks should not be used with cancelability enabled. The next example shows how to prevent data inconsistency using cleanup handlers. However, this example shows a typical use of condition variables.
A long lock has the long_lock_t data type. It must be initialized by the long_lock_init routine. The long_lock, long_trylock, and long_unlock subroutine performs similar operations to the pthread_mutex_lock, pthread_mutex_trylock, and pthread_mutex_unlock subroutine.
typedef struct { pthread_mutex_t lock; pthread_cond_t cond; int free; int wanted; } long_lock_t;
void long_lock_init(long_lock_t *ll) { pthread_mutex_init(&ll->lock, NULL); pthread_cond_init(&ll->cond); ll->free = 1; ll->wanted = 0; }
void long_lock_destroy(long_lock_t *ll) { pthread_mutex_destroy(&ll->lock); pthread_cond_destroy(&ll->cond); }
void long_lock(long_lock_t *ll) { pthread_mutex_lock(&ll->lock); ll->wanted++; while(!ll->free) pthread_cond_wait(&ll->cond); ll->wanted--; ll->free = 0; pthread_mutex_unlock(&ll->lock); }
int long_trylock(long_lock_t *ll) { int got_the_lock; pthread_mutex_lock(&ll->lock); got_the_lock = ll->free; if (got_the_lock) ll->free = 0; pthread_mutex_unlock(&ll->lock); return got_the_lock; }
void long_unlock(long_lock_t *ll) { pthread_mutex_lock(&ll->lock); ll->free = 1; if (ll->wanted) pthread_cond_signal(&ll->cond); pthread_mutex_unlock(&ll->lock); }
Traditional semaphores in UNIX systems are interprocess synchronization facilities. It is possible to implement interthread semaphores for specific usage.
The following implementation is very basic. Error handling is not performed, but cancellations are properly handled with cleanup handlers whenever required.
A semaphore has the sema_t data type. It must be initialized by the sema_init routine and destroyed with the sema_destroy routine. The P and V operations are respectively performed by the sema_p and sema_v routines.
typedef struct { pthread_mutex_t lock; pthread_cond_t cond; int count; } sema_t;
void sema_init(sema_t *sem) { pthread_mutex_init(&sem->lock, NULL); pthread_cond_init(&sem->cond, NULL); sem->count = 1; }
void sema_destroy(sema_t *sem) { pthread_mutex_destroy(&sem->lock); pthread_cond_destroy(&sem->cond); }
void p_operation_cleanup(void *arg) { sema_t *sem; sem = (sema_t *)arg; pthread_mutex_unlock(&sem->lock); }
void sema_p(sema_t *sem) { pthread_mutex_lock(&sem->lock); pthread_cleanup_push(p_operation_cleanup, sem); while (sem->count <= 0) pthread_cond_wait(&sem->cond, &sem->lock); sem->count--; /* * Note that the pthread_cleanup_pop subroutine will * execute the p_operation_cleanup routine */ pthread_cleanup_pop(1); }
void sema_v(sema_t *sem) { pthread_mutex_lock(&sem->lock); sem->count++; if (sem->count <= 0) pthread_cond_signal(&sem->cond); pthread_mutex_unlock(&sem->lock); }
The counter specifies the number of users that are allowed to take the semaphore. It is never strictly negative; thus, it does not specify the number of waiting users, as for traditional semaphores. This implementation provides a typical solution to the multiple wakeup problem on the pthread_cond_wait subroutine. Note that the P operation is cancelable, because the pthread_cond_wait subroutine provides a cancellation point.
A write-priority read/write lock provides multiple threads simultaneous read-only access to a protected resource, and a single thread write access to the resource while excluding reads. When a writer releases a lock, other waiting writers will get the lock before any waiting reader. Write-priority read/write locks are usually used to protect resources that are more often read than written.
The following implementation is very basic. The lock owner is not checked, any thread can unlock any lock. Routines similar to the pthread_mutex_trylock subroutine are missing and error handling is not performed, but cancellations are properly handled with cleanup handlers whenever required.
A write-priority read/write lock has the rwlock_t data type. It must be initialized by the rwlock_init routine. The rwlock_lock_read routine locks the lock for a reader (multiple readers are allowed), the rwlock_unlock_read routine unlocks it. The rwlock_lock_write routine locks the lock for a writer, the rwlock_unlock_write routine unlocks it. The proper unlocking routine (for the reader or for the writer) must be called.
typedef struct { pthread_mutex_t lock; pthread_cond_t rcond; pthread_cond_t wcond; int lock_count; /* < 0 .. held by writer */ /* > 0 .. held by lock_count readers */ /* = 0 .. held by nobody */ int waiting_writers; /* count of wating writers */ } rwlock_t;
void rwlock_init(rwlock_t *rwl) { pthread_mutex_init(&rwl->lock, NULL); pthread_cond_init(&rwl->wcond, NULL); pthread_cond_init(&rwl->rcond, NULL); rwl->lock_count = 0; rwl->waiting_writers = 0; }
void waiting_reader_cleanup(void *arg) { rwlock_t *rwl; rwl = (rwlock_t *)arg; pthread_mutex_unlock(&rwl->lock); }
void rwlock_lock_read(rwlock_t *rwl) { pthread_mutex_lock(&rwl->lock); pthread_cleanup_push(waiting_reader_cleanup, rwl); while ((rwl->lock_count < 0) && (rwl->waiting_writers)) pthread_cond_wait(&rwl->rcond, &rwl->lock); rwl->lock_count++; /* * Note that the pthread_cleanup_pop subroutine will * execute the waiting_reader_cleanup routine */ pthread_cleanup_pop(1); }
void rwlock_unlock_read(rwlock_t *rwl) { pthread_mutex_lock(&rwl->lock); rwl->lock_count--; if (!rwl->lock_count) pthread_cond_signal(&rwl->wcond); pthread_mutex_unlock(&rwl->lock); }
void waiting_writer_cleanup(void *arg) { rwlock_t *rwl; rwl = (rwlock_t *)arg; rwl->waiting_writers--; if ((!rwl->waiting_writers) && (rwl->lock_count >= 0)) /* * This only happens if we have been canceled */ pthread_cond_broadcast(&rwl->wcond); pthread_mutex_unlock(&rwl->lock); }
void rwlock_lock_write(rwlock_t *rwl) { pthread_mutex_lock(&rwl->lock); rwl->waiting_writers++; pthread_cleanup_push(waiting_writer_cleanup, rwl); while (rwl->lock_count) pthread_cond_wait(&rwl->wcond, &rwl->lock); rwl->lock_count = -1; /* * Note that the pthread_cleanup_pop subroutine will * execute the waiting_writer_cleanup routine */ pthread_cleanup_pop(1); }
void rwlock_unlock_write(rwlock_t *rwl) { pthread_mutex_lock(&rwl->lock); l->lock_count = 0; if (!rwl->wating_writers) pthread_cond_broadcast(&rwl->rcond); else pthread_cond_signal(&rwl->wcond); pthread_mutex_unlock(&rwl->lock); }
Readers are just counted. When the count reaches zero, a waiting writer may take the lock. Only one writer can hold the lock. When the lock is released by a writer, another writer is awakened, if there is one. Otherwise, all waiting readers are awakened.
Note that the locking routines are cancelable, because they call the pthread_cond_wait subroutine. For this reason, cleanup handlers are registered before calling the subroutine.
List of Threads Advanced Feature Subroutines.