PREV UP NEXT StackThreads/MP: version 0.77 User's Guide

3.4.1: Initialization, Lock, and Unlock

        typedef struct st_int_loc st_int_loc_t;
@        typedef struct st_long_loc st_long_loc_t;
@        typedef struct st_ptr_loc st_ptr_loc_t;

@        int st_read_and_lock_int(il);
@        long st_read_and_lock_long(ll);
@        void * st_read_and_lock_ptr(pl);

@        void st_write_and_unlock_int(il, i);
@        void st_write_and_unlock_long(ll, l);
@        void st_write_and_unlock_ptr(pl, p);

@        int ST_INT_LOC_INIT(il, i);
@        long ST_LONG_LOC_INIT(ll, l);
@        void * ST_PTR_LOC_INIT(pl, p);

@            st_int_loc_t * il;   int i;
@            st_long_loc_t * ll;  long l;
@            st_ptr_loc_t * pl;   void * p;

@        st_int_loc_t v = ST_INT_LOC_INITIALIZER(x);
@        st_long_loc_t v = ST_LONG_LOC_INITIALIZER(x);
@        st_ptr_loc_t v = ST_PTR_LOC_INITIALIZER(x);

Type st_int_loc_t is int type that is `tagged' by `lock bits.' Similarly, st_long_loc_t and st_ptr_loc_t are tagged long and void *, respectively.

st_read_and_lock_int(il) waits for the location pointed to by il to become unlocked. When it is unlocked, it atomically lock the location and returns the value previously written at the location pointed to by il. Lock bits are stripped. st_read_and_lock_long(ll) and st_read_and_lock_ptr(pl) work similarly, but on locations that hold values of type st_loc_long_t and st_loc_ptr_t, respectively.

st_write_and_unlock_int(il, i) writes value i to the location pointed to by il and unlocks the location. The location must be locked upon calling the procedure. st_write_and_unlock_long(ll, l) and st_write_and_unlock_ptr(pl, p) are similar procedures for st_long_loc_t * and st_ptr_loc_t *, respectively.

ST_INT_LOC_INT(il, i) initializes the location pointed to by il by i. The location is initialized to `unlocked' state. ST_LONG_LOC_INT(ll, l) and ST_PTR_LOC_INT(pl, p) are similar procedures for st_long_loc_t * and st_ptr_loc_t *, respectively.

st_int_loc_t v = ST_INT_LOC_INITIALIZER(x); defines a global or static variable of st_int_loc_t that is initialized to x. ST_LONG_LOC_INITIALIZER(x) and ST_PTR_LOC_INITIALIZER(x) are similar initializers for st_long_loc_t and st_ptr_loc_t, respectively.

st_read_and_lock and st_write_and_unlock can be used to perform an atomic read-modify-write sequence on a single location. Here is a typical example.

        st_int_loc_t il = ST_INT_LOC_INITIALIZER(1);
@
@        void read_modify_write()
@        {
@            int x = st_read_and_lock_int(&il);
@            st_write_and_unlock_int(&il, 3 * x);
@        }

This code atomically multiplies the value at the location il by 3.

Remark 1: Primitives st_read_and_lock_xxx never block the calling thread; they just wait for the location to become unlocked by spinning. Therefore, do every effort to make the critical section as short as possible. How short should they be? Well, as a rough approximation, avoid anything more than a couple of arithmetics and a couple of loads and stores. Avoid dynamic memory allocations such as malloc when you are holding a lock. Also, never try to lock a location when you are locking another location. It is a common source of deadlocks.

If you cannot avoid having a long critical section, you should not use spin-lock primitives in the first place, but use blocking synchronization primitives. As already shown, StackThreads/MP provides synchronization primitives that block the calling thread and schedule another thread when necessary. st_mutex_t will be most useful for such purposes.

Nonetheless, spin-locks are useful for short-critical sections. You cannot unconditionally adopt blocking primitives, as critical sections can be made short in many cases and in such cases spin-locks are both time- and space- efficient.

To avoid deadlocks and severe performance degradation due to a wrong use of spin-locks, st_read_and_lock primitives will timeout after a certain number of attempts. More specifically, when they fail to obtain a lock 1,000 times in a row, they sleep the calling worker 1 millisecond and retry. They repeat this process 10 times. When the lock is not still granted, it displays an error message and the application dies. It may kill an application that would otherwise be correct. However, it can detect a common programming error in which the programmer forgets to write a necessary unlock operation. Even if the application works, holding a lock 10 milliseconds typically degrades performance of the application, so you should take the error message as a strong suggestion that your locking strategy is wrong.

Remark 2: These primitives assume that two bits are embedded for tags within a single word. For example, on machines where int is 32 bits, st_int_loc_t uses the least significant two bits for tags. The value is left-shifted by two bits and stored in the most significant 30 bits. Consequently the most significant two bits are discarded when a value is stored. Conversely, when a value is read, the most significant two bits are sign-extended. Therefore, values representable by st_int_loc_t are restricted to -2^30 ... 2^30 - 1. For pointers, we assume the value of a pointer is 4 bytes aligned. That is, the least significant two bits of a pointer are zeroes and we use these two bits for tags. When a value that is not properly aligned is given to st_write_and_unlock_ptr, the least significant two bits are just discarded.