[ Previous | Next | Contents | Glossary | Home | Search ]
AIX Version 4.3 General Programming Concepts: Writing and Debugging Programs

Creating Locking Services

Some programmers may want to implement their own high-level locking services instead of using the standard locking services (mutexes) provided in the threads library. For example, a database product may already use a set of internally defined services: it can be easier to adapt these locking services to a new system than to adapt all the internal modules that use these services.

For this reason, AIX provides atomic locking service primitives which can be used to build higher level locking services. To create services that are multiprocessor-safe (like the standard mutex services), programmers must use the atomic locking services described in this section and not atomic operations services, such as compare_and_swap.

Multiprocessor-Safe Locking Services

Locking services are used to serialize access to resources that may be used concurrently. For example, insertions in a linked list require several pointer updates. If the update sequence by one process is interrupted by a second process which tries to access the same list, an error can occur. A sequence of operations that should not be interrupted is called a critical section.

Locking services use a lock word to indicate the lock status: 0 (zero) can be used for free, and 1 (one) for busy. Therefore, a service to acquire a lock would do the following:

test the lock word
if the lock is free
        set the lock word to busy
        return SUCCESS
... 

Because this sequence of operations (read, test, set) is itself a critical section, special handling is required. On a uniprocessor system, disabling interrupts during the critical section prevents interruption by a context switch. But on a multiprocessor system, the hardware must provide a so-called test-and-set primitive, usually with a special machine instruction. In addition, special processor dependent synchronization instructions called import and export fences are used to temporarily block other reads or writes. They protect against concurrent access by several processors and against the read and write reordering performed by modern processors.

To mask this complexity and provide independence from these machine-dependent instructions, three subroutines are defined:

_check_lock Conditionally updates a single word variable atomically, issuing an import fence for multiprocessor systems. The compare_and_swap routine is similar, but it does not issue an import fence and, therefore, is not usable to implement a lock.
_clear_lock Atomically writes a single word variable, issuing an export fence for multiprocessor systems.
_safe_fetch Atomically reads a single word variable, issuing an import fence for multiprocessor systems. The import fence ensures that the read value is not a stale value resulting from an early pre-fetch. This subroutine is rarely needed.

Locking Services Example

These routines can be used to create custom high-level routines independent of the threads library. The example that follows shows partial implementations of subroutines similar to the pthread_mutex_lock and pthread_mutex_unlock subroutines in the threads library:

#include <sys/atomic_op.h>       /* for locking primitives */
#define SUCCESS          0
#define FAILURE          -1
#define LOCK_FREE        0
#define LOCK_TAKEN       1
typdef struct {
        atomic_p         lock;   /* lock word */
        tid_t            owner;  /* identifies the lock owner */
        ...              /* implementation dependent fields */
} my_mutex_t;
...
int my_mutex_lock(my_mutex_t *mutex)
{
tid_t   self;   /* caller's identifier */
  
        /*
        Perform various checks:
          is mutex a valid pointer?
          has the mutex been initialized?
        */
        ...
        /* test that the caller does not have the mutex */
        self = thread_self();
        if (mutex->owner == self)
                return FAILURE;
        /*
        Perform a test-and-set primitive in a loop.
        In this implementation, yield the processor if failure.
        Other solutions include: spin (continuously check);
                 or yield after a fixed number of checks.
        */
        while (!_check_lock(&mutex->lock, LOCK_FREE, LOCK_TAKEN))
                yield();
        mutex->owner = self;
        return SUCCESS;
} /* end of my_mutex_lock */
int my_mutex_unlock(my_mutex_t *mutex)
{
        /*
        Perform various checks:
          is mutex a valid pointer?
          has the mutex been initialized?
        */
        ...
        /* test that the caller owns the mutex */
        if (mutex->owner != thread_self())
                return FAILURE;
        _clear_lock(&mutex->word, LOCK_FREE);
        return SUCCESS;
} /* end of my_mutex_unlock */

Related Information

Programming on Multiprocessor Systems: Overview


[ Previous | Next | Contents | Glossary | Home | Search ]