Synchronization Objects

Synchronization Objects

There are four basic synchronization mechanisms supported by Posix threads and most other threading models. These are the mutual exclusion lock, condition variables , semaphores, and reader/writer locks. A brief discussion of each synchronization object and where you might want to use them follows . The function calls presented are all from the Solaris threads library. Most Posix based multithreading libraries will use the identical, or at least very similar, functions.

The most basic synchronization primitive for multithreaded programming is the mutual exclusion lock. This is the most efficient mechanism in terms of both memory utilization and execution time. A mutual exclusion lock is used to serialize access to a resource but before using it you must initialize it with the

 mutex_init() 

function. When a thread is ready to access a critical section, you use the

 mutex_lock() 

function to acquire the lock. This blocks the thread if the mutex is already locked by another process. To avoid "spinning" on a locked mutex, you can also test the state of a mutex with the

 mutex_trylock() 

call. After completing the critical section, you must call

 mutex_unlock() 

to release the lock. Finally, when you will no longer need the mutex lock you should call

 mutex_destroy() 

to free the lock.

When you are using multiple mutex locks, one of the things you need to avoid is the potential for deadlock. Deadlock occurs when two threads end up waiting for each other to release a lock. Here is a simple example of a deadlock situation.

 void thread1() {     mutex_lock(m1)     /* use resource m1 */              mutex_lock(m2)         /* use resources m1 and m2 */                  mutex_unlock(m2)    mutex_unlock(m1) } void thread2() {     mutex_lock(m2)     /* use resource 2 */         mutex_lock(m1)         /* use resources m1 and m2 */                  mutex_unlock(m1)     mutex_unlock(m2) } 

In the above example, if thread 1 acquires the mutex lock m1, then thread 2 acquires the mutex lock m2 before thread 1 does, each thread will block, waiting for the other mutex lock to be released. Thread 1 will not release lock m1 until it can acquire lock m2. Thread 2 will not release lock m2 until it can acquire lock m1. In this case, we have a deadlock and neither thread will ever complete.

The simplest way to avoid this type of deadlock is to make sure threads locking multiple mutexes always do so in the same order. This technique is called lock hierarchies. The Unix lock_lint tool can be used to detect deadlocks of this kind. If it is not possible to use lock hierarchies, then the programmer must use the mutex_trylock() function to detect a possible deadlock and manually release any locks it holds and retry the operation.

The next most efficient synchronization primitive is the condition variable. A condition variable is used to block on a change of state. You use a condition lock to block threads until a particular condition is true. Commonly, condition variables are used to implement monitors . Before using a condition lock, you must first initialize it with the

 cond_init() 

call. Afterwards, a call to

 cond_wait() 

will block until the specified condition is true. You use the

 cond_signal() 

call to unblock a specific thread. Other condition variable calls include

 cond_timedwait() 

to block until a specified event,

 cond_broadcast() 

to unblock all threads and

 cond_destroy() 

to destroy a condition variable when you are finished with it.

Another synchronization object is the semaphore. Semaphores use more memory than condition variables, however, they are easier to use in many instances. Conceptually, a semaphore is a non-negative integer count. Semaphores are typically used to coordinate access to resources, with the semaphore count initialized to the number of free resources. Threads increment the count as resources are added and decrement it as they are removed. When the semaphore count reaches 0, it indicates that no more resources are available. A semaphore is initialized with:

 sema_init() 

You increment a semaphore with

 sema_post() 

and decrement it with

 sema_trywait() 

Once you are done using a semaphore, you destroy it with

 sema_destroy() 

The most complex thread synchronization mechanism is the reader/writer lock. A reader/writer lock is useful for resources whose contents are searched more often than they are changed. A reader/writer lock allows simultaneous read access by many threads while restricting write access to only one thread at a time.

A reader/writer lock is initialized with the

 rwlock_init() 

call. A thread acquires a read lock with the

 rw_rdlock() 

call. This call will succeed as long as the lock is not already being held by a writer thread using the

 rw_wrlock() 

call. When any thread holds the lock for reading, other threads can also acquire the lock for reading but must wait to acquire the lock for writing. Writer starvation is avoided because once a thread is waiting on a write lock, no further read locks will succeed until the write lock has been successfully acquired and released.

To unlock either a read or write lock, the

 rw_unlock() 

call is used. Once a programmer is completely finished using a reader/writer lock, it should be destroyed with

 rwlock_destroy() 


Software Development. Building Reliable Systems
Software Development: Building Reliable Systems
ISBN: 0130812463
EAN: 2147483647
Year: 1998
Pages: 193
Authors: Marc Hamilton

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net