#$Introduction
Initialization
Mutexes
ReadWrite Locks
Condition Variables
Locking Hierarchies
Debugging

#$+The OSI package provides very efficient locking and synchronization primitives for the Win32 environment.
These primitives include both mutexes, representing resources that must always be allocated exclusively to one entity (thread) at a time, as well as read/write locks, representing resources that may have N readers or one writer at any given time.
In addition to the basic primitives sketched above, the OSI package provides a condition variable mechanism that allows a thread holding a mutex or a read/write lock to wait for an interesting event to occur.  These condition variable operations allow a thread holding a resource to atomically suspend itself (sleep) and simultaneously release the lock or mutex it holds on the resource.  The atomicity of these operations is critical to avoiding race conditions.  A function is also provided to wakeup a thread waiting at a conditionvariable. 
Some support is also provided for aiding programs in initializing themselves safely in a multi-threaded environment.
Finally, an optional remote debugging and statistics gathering interface is provided.  If desired, the locks and mutexes provided by the OSI package can keep track of statistics on lock contention, and these statistics are available to remote users to examine, using the osidebugprogram. 

#$+There are two sets of routines describe herein, one used for initializing the OSI package itself, and one for initializing your application. 
The function osi_Init must be called before any other functions in the OSI library, except for the other initialization functions described in this section (osi_Init, osi_InitDebug, osi_Once and osi_TestOnce).
The function osi_InitDebug must be called before remote debugging information and statistic can be retrieved via RPC by the osidebug program.  This function need not be ever called, however, if you do not need to be able to retrieve locking information from a separate process. 
The function osi_Once can be used by the application programmer to ensure that the application program executes its initialization code exactly once.  This function takes one parameter, intended to be allocated as a static variable, which will be initialized by the system at load time to zero.  The osi_Once function is called with a pointer to this variable, and returns TRUE if the program should execute its initialization code now, or FALSE if osi_Once has already been executed once in this application already. 
The function osi_EndOnce must be called before other calls to osi_Once will complete; the section of code bounded by osi_Once and osi_EndOnce is executed atomically with respect to other calls to osi_Once. 
The function osi_TestOnce may be used as a hint to see if osi_Once might need to be called.  It returns the same values as osi_Once, but doesn't change the state of the state variable to indicate that osi_Once has been called.  Of course, its result can only be used as a hint, but if osi_TestOnce returns false, the caller knows that the initialization work for this code has already run.  If it returns true, the programmer may still need to call osi_Once, or it may be called by some other thread first.

#$K+void osi_Init(void)
This function initializes the OSI library.  It must be called before any other functions in the OSI library except the other initialization calls: osi_InitDebug, osi_Once, osi_EndOnce and osi_TestOnce.
Failure to call this function will result in traps in your application program.

#$K+long osi_InitDebug (char *rpcName)
This function initializes the RDC debugging interface in your application.  The parameter rpcName is an RPC NSI name into which your RPC binding information will be exported.  It must start with the string "/.:/"  For example, a database server for your payroll database might export its binding information into "/.:/payroll"
The function returns 0 if it succeeds, otherwise it returns an RPC error code. 

#$K+int osi_Once (osi_once_t *parm)
This function can be used by application programs in a multi-threaded environment to ensure that they execute their initialization code exactly once, no matter how many threads try to execute the code.
int foo_Init(void) 
{ 
    static osi_once_t once; 
    if (osi_Once(&once)) { 
        YourInitializationHere = 1; 
        osi_EndOnce(&once); 
    } 
    return SUCCESS; 
} 

In the above function, the assignment into YourInitializationHere will occur exactly once, no matter how many times foo_Init is called, in any number of threads, all calling foo_Init concurrently.
#$K+void osi_EndOnce (osi_once_t *parm)
This function ends the initialization block started by osi_Once.  This function must be called exactly once for each call to osi_Once that returns true.
An example of the use of this function can be found in the description of osi_Once.
#$K+int osi_TestOnce (osi_once_t *parm)
This function returns true if osi_Once would have returned true, and false otherwise.
Unlike osi_Once, this function does not mark the initialization block described by *parm as initialized; this is only an advisory function.  For this reason, the function osi_EndOnce should not be called if osi_TestOnce returns true. 
#$+Mutual exclusion objects are used to prevent two threads from accesing the same object at the same time.  Typically, some collection of data is associated with an object of osi_mutex_t, and any thread, before accessing the object, first obtains the mutex by calling lock_ObtainMutex.  When it is done processing the object, the thread calls lock_ReleaseMutex. 
Typically, a thread will release a mutex once for each time that it  obtains the mutex, using osi_SleepM or lock_ReleaseMutex. 
These mutex objects are not recursive mutexes: a thread may not re-obtain the mutex while still holding it.
Before using a mutex, the mutex must be initialized by calling lock_InitializeMutex.
If the mutex is allocated from dynamically allocated memory (rather than being statically allocated), then the mutex must be finalized before the memory is freed.  The function lock_FinalizeMutex should be used for this purpose.
An object of type osi_mutex_t uses about 8 bytes of storage.

#$K+void lock_InitializeMutex (osi_mutex_t *mutex)
This function is called with a pointer to a mutex, to initialize the storage so that the mutex can be obtained and released by this or other threads. This function should only be called at an initialization point where the programmer knows that no other threads are accessing the mutex variable.

#$K+void lock_ObtainMutex (osi_mutex_t *mutex)
This function is called with a pointer to a mutex, and waits until no other thread is using the mutex.  It then obtains the mutex for the calling thread and returns.
Mutexes are not recursively obtainable by a thread: if a thread tries to obtain a mutex a second time, it will deadlock (wait forever). 
The mutex can be released lock_ReleaseMutex or by osi_SleepM.

#$K+void lock_ReleaseMutex (osi_mutex_t *mutex)
This function relinquishes the caller's use of the resource; if any other threads are waiting for the mutex, they are woken at this point, and one of the other threads is given the mutex.
It is an error to release a mutex twice.  However, a mutex can be released by a different thread than that which obtained it, as long as this is the programmer's intention.

#$K+void lock_FinalizeMutex (osi_mutex_t *mutex)
This function is used to free up any auxiliary storage and control structures associated with a mutex.  In the current implementation, this function does nothing except when running with the statistics gathering implementation of mutexes, but this is not guaranteed to stay true in later releases of the OSI library.  Furthermore, anyone can enable statistics gathering.
Locks and mutexes need not be finalized if the process is about to terminate, since any resources allocated are allocated on behalf of the process, and will be freed when the process exits.
#$K+int lock_TryMutex (osi_mutex_t *mutexp)
This function is a non-blocking version of lock_ObtainMutex.  It tries to obtain the specified mutex, and if it succeeds, it returns 1.  Otherwise, instead of blocking and waiting until the mutex is available, it returns 0 and leaves the mutex unchanged.
This function is typically used when the programmer needs to obtain locks in an order incompatible with their locking hierarchy.  See the section on locking hierarchies  for an example of its use for this purpose.
The caller must be careful to release the mutex if and only if the function returns 1.

#$+Read/write locks are similar in use to mutexes.  They are used to mediate access to data structures that are accessed by multiple threads.
Unlike mutexes, however, read/write locks mediate two forms of access to the structures: read accesses, of which several can be executing concurrently, and write accesses, only one of which can be executing at any given time.
Like mutexes, read/write locks must be initialized before they can be used, and need to be finalized when their storage is going to be freed.  The procedureslock_InitializeRWLock and lock_FinalizeRWLock perform these functions.
#$K+void lock_InitializeRWLock (osi_rwlock_t *lockp)
This function initializes the storage used by a read/write lock.  The structure must be initialized before it is obtained or released by the functions described in this section.

#$K+void lock_ObtainRead (osi_rwlock_t *lockp)
This function obtains a read/write lock for reading.  The lock must have beenpreviously initialized with lock_InitializeRWLock.  
When done with the lock, the programmer must ensure that the function lock_ReleaseRead or osi_SleepR is called to release the lock. 
#$K+void lock_ObtainWrite (osi_rwlock_t *lockp)
This function obtains a read/write lock for writing.  The lock must have been previously initialized with lock_InitializeRWLock.
At most one thread can have a read/write lock held for writing at any time, and no thread can simultaneously have a read/write lock held for reading at that time.
When done with the lock, the programmer must ensure that the function lock_ReleaseWrite or osi_SleepW is called to release the lock.
#$K+void lock_ReadRead(osi_rwlock_t *lockp)
This function releases a lock held in read mode.  If the number of readers drops to zero, a writer may obtain the lock, otherwise only readers may obtain the lock.  If any writers are waiting for this lock, they are woken up and may proceed at this time.
The application program must have obtained this lock in read mode before calling this function; it is an error to release a read lock more often than it was obtained.

#$K+void lock_ReleaseWrite(osi_rwlock_t *lockp)
This function releases a lock held in write mode.  After this call, anyone waiting for a read lock or a write lock is woken up and may proceed.
The application program must have obtained this lock in read mode before calling this function; it is an error to release a read lock more often than it was obtained.

#$K+void lock_FinalizeRWLock (osi_rwlock_t *lockp)
This function is called to free up any auxiliary data structures associated with the read/write lock.  This function must be called before freeing any storage containing a read/write lock.  This function must be called at a time when there are no threads holding or waiting for the lock concerned.
Locks allocated from static storage need never be finalized.

#$K+int lock_TryRead(osi_rwlock_t *rwlockp)
This function is a non-blocking version of lock_ObtainRead.   It tries to obtain the specified read/write lock in read mode, and if it succeeds, it returns 1.  Otherwise,  instead of blocking and waiting until the lock is available, it returns 0 and leaves the lock unchanged. 
This function is typically used when the programmer needs to obtain locks in an order incompatible with their locking hierarchy.  See the section on locking hierarchies  for an example of its use for this purpose.
The caller must be careful to release the read lock if and only if the function returns 1.

#$K+int lock_TryWrite(osi_rwlock_t *rwlockp)
This function is a non-blocking version of lock_ObtainWrite.   It tries to obtain the specified read/write lock in write mode, and if it succeeds, it returns 1.  Otherwise,  instead of blocking and waiting until the lock is available, it returns 0 and leaves the lock unchanged. 
This function is typically used when the programmer needs to obtain locks in an order incompatible with their locking hierarchy.  See the section on locking hierarchies  for an example of its use for this purpose.
The caller must be careful to release the write lock if and only if the function returns 1.

#$+Condition variables are used to allow a threaded program to wait for an interesting event.
Condition variables are represented as long integers.  By convention, these long integers are cast from addresses of mnemonic structures (so that, when waiting for a particular structure, foo, to change state, you might sleep on "(long)&foo"), but the sleep functions do not change any storage at the address named by the condition variable, and indeed, condition variable integers can really be any numbers you desire.
The basic idea is that a thread that wants to wait for a particular event waits for a condition variable by calling osi_Sleep with the condition variable as a parameter, which blocks the calling thread.  When the interesting event occurs, the thread that notices the event calls osi_Wakeup , waking up allthreads sleeping on that condition variable.
Unfortunately, condition variables as described so far are unusable in most cases, due to a race condition that occurs in their typical uses.  Here's an example: assume that there is a structure that represents a buffer of data.  Some threads read from the buffer, waiting until there is data available, and others add data to the buffer, waiting until there is space available.  The structure, read and write calls might be coded like this:
#define BSIZE	100	/* buffer size */

typedef struct buffer {
    char data[BSIZE];   /* buffer */
    int nbytes;         /* bytes used in the buffer */
    osi_mutex_t mutex;	/* mutex for synchronizing access */
    BOOL readWaiting;   /* true if someone is waiting for data */
    BOOL writeWaiting;  /* true if someone is waiting for space */
} buffer_t;

int Read(buffer_t *bp, char *data)
{
    long bytesRead; 
    
    while(1) { 
        lock_ObtainMutex(&bp->mutex);
        if (nbytes == 0) { /* no data available */
            readWaiting = 1;
            /* wait for something interesting */
            lock_ReleaseMutex(&bp->mutex);      /*BUG*/
            osi_Sleep((long) &bp->readWaiting); /*BUG*/
            continue; 
        } 
        
        memcpy(bp->data, data, bp->nbytes);
        nbytes = bp->nbytes; /* remember for later */ 
        bp->nbytes = 0; 
        
        /* now wakeup anyone waiting for space */ 
        if (bp->writeWaiting) { 
            bp->writeWaiting = 0; 
            osi_Wakeup((long) &bp->writeWaiting); 
        } 
        
        /* and we're done, so return # of bytes read */ 
        lock_ReleaseMutex(&bp->mutex);
        return nbytes;
    }
}

void Write(buffer_t *bp, char *data, int len) 
{ 
    while(1) { 
        lock_ObtainMutex(&bp->mutex); 
        if (len > BSIZE-bp->nbytes) { 
            /* no space available */
            writeWaiting = 1;
            /* wait for something interesting */
            lock_ReleaseMutex(&bp->mutex);       /*BUG*/ 
            osi_Sleep((long) &bp->writeWaiting); /*BUG*/ 
            continue; 
        } 
        
        memcpy(data, bp->data+bp->nbytes, len);
        bp->nbytes += len; 
        
        /* now wakeup anyone waiting for data */ 
        if (bp->readWaiting) { 
            bp->readWaiting = 0; 
            osi_Wakeup((long) &bp->readWaiting); 
        } 
        
        /* and we're done, so return # of bytes read */ 
        lock_ReleaseMutex(&bp->mutex);
        return ;
    }
} 

There are a number of things to note in the above example.  First, note our use of memory addresses as condition variables.  The calls to osi_Sleep and osi_Wakeup do not change the data at bp->readWaiting or bp->writeWaiting, but the call osi_Wakeup((long) &bp->readWaiting)only wakes up the thread sleeping in the call osi_Sleep((long) &bp->readWaiting), not osi_Sleep((long) &bp->writeWaiting) .
Please also note that accesses by multiple threads to this buffer structure are mediated by a mutex structure stored in the buffer itself.  Whenever either Reador Writeblock waiting for the other to supply/remove data, they of course must release the mutex so that the other thread can access the structure to supply/remove the data.
Unfortunately, there is also a bug in the above code: consider the case where the Readcall is interrupted due to a clock interrupt between the time that a thread releases the mutex and  the time that the thread then calls osi_Sleep.  Assume that the OS does a context switch to a new thread, that calls Write, and runs through completely, doing the call to osi_Wakeup.  The OS then switches back to the thread doing the read, and the thread proceeds and calls osi_Sleep, even though there is data now waiting.  Unfortunately, no one is going to wake us up to notice that there is data available now.
This problem would be solved if it were impossible for a wakeup call to execute between the time that the mutex was released and the time that the thread blocked in osi_Sleep.  This combination of releasing a lock or a mutex and sleeping on a condition variable is provided by three functions, osi_SleepM, osi_SleepR and osi_SleepW, which atomically sleep and release a mutex, read lock and write lock, respectively.
Thus, the two pairs of lines in the example source above that are labelled with /*BUG*/should be replaced by osi_SleepM((long) &bp->readWaiting, &bp->mutex) and osi_SleepM((long) &bp->writeWaiting, &bp->mutex), respectively.Indeed, almost every use of osi_Sleepinstead of one of the combination routines described above is incorrect, and has a race condition between the last release of a lock or mutex, and the call to osi_Sleep.  So, please be careful when using osi_Sleepto be sure to check for this condition.

#$K+void osi_SleepM(long sleepValue, osi_mutex_t *mutex)
This function atomically sleeps at sleepValueand releases the mutex at mutex.  Atomically in this case means that no osi_Wakeupcalls will be executed from before the lock is released until after the thread is asleep.
The programmer must always be prepared for rare, spontaneous wakeups from any of the osi_Sleepfamily of functions.  Thus, any use of osi_SleepMshould be contained within a while loop that rechecks the appropriate condition that the thread is waiting for, and tries sleeping again if the wakeup was a spurious one.

#$K+void osi_SleepR(long sleepValue, osi_rwlock_t *lockp)
This function atomically sleeps at sleepValueand releases the read lock at lockp.  Atomically in this case means that no osi_Wakeupcalls will be executed from before the lock is released until after the thread is asleep.
The programmer must always be prepared for rare, spontaneous wakeups from any of the osi_Sleepfamily of functions.  Thus, any use of osi_SleepRshould be contained within a while loop that rechecks the appropriate condition that the thread is waiting for, and tries sleeping again if the wakeup was a spurious one.

#$K+void osi_SleepW(long sleepValue, osi_rwlock_t *lockp)
This function atomically sleeps at sleepValueand releases the write lock at lockp.  Atomically in this case means that no osi_Wakeupcalls will be executed from before the lock is released until after the thread is asleep.
The programmer must always be prepared for rare, spontaneous wakeups from any of the osi_Sleepfamily of functions.  Thus, any use of osi_SleepWshould be contained within a while loop that rechecks the appropriate condition that the thread is waiting for, and tries sleeping again if the wakeup was a spurious one.

#$K+void osi_Sleep(long sleepValue)
This function sleeps at sleepValue.  The thread will not resume execution until after another thread later executes a call to osi_Wakeupwith the same value for sleepValue.
The programmer must always be prepared for rare, spontaneous wakeups from any of the osi_Sleepfamily of functions.  Thus, any use of osi_Sleepshould be contained within a while loop that rechecks the appropriate condition that the thread is waiting for, and tries sleeping again if the wakeup was a spurious one.
Also remember that almost every use of this function when waiting for a change in data protected by a mutex or read/write lock is incorrect, and you probably should be using one of osi_SleepM, osi_SleepR, or osi_SleepW to simultaneously release the data protection lock and sleep atomically.
#$K+void osi_Wakeup(long sleepValue)
This function wakes up all threads already sleeping at sleepValue.  Threads sleepby calling osi_Sleep, osi_SleepM, osi_SleepR, or osi_SleepW.
This call will synchronize appropriately with osi_SleepM, osi_SleepR, and osi_SleepW, so that if it is called while the corresponding lock or mutex is held, the osi_Wakeup function won't execute until after the thread sleeping on the condition variable is blocked.  This avoid race conditions of the form described in the section on condition variables.

#$+Whenever a thread obtains more than one lock at a time, there is a possibility of deadlock if the locks are obtained in the wrong order.  For example, imagine a situation where there are two threads, numbered 1 and 2, and two locks, named A and B.  If thread 1 tries to obtain lock A first and then lock B, while thread 2 tries to lock B and then lock A, these two threads will, on occasion, deadlock.
Specifically, what can happen is that thread 1 can obtain lock A, and then lose the processor to thread 2, which then goes on to obtain lock B.  Thread 2, after obtaining lock B, tries to obtain lock A, and blocks, since thread 1 already holds this lock.  Eventually, thread 1 runs again, and tries to obtain lock B, but this lock is held (by thread 2), and so thread 1 blocks, still holding lock A.  This is a classical case of a deadlock, or deadly embrace.
The heart of the problem is that two locks are obtained at the sametime, but in different orders by different threads.  And so, thisproblem can be solved in several different ways, some of which will bediscussed here.The easiest way to solve this problem is to have a global locking order, and have all threads lock their locks in this ordering.  It is of course imperative to come up with a reasonable locking hierarchy (ordering) to make this as easy as possible.  This solution is particularly good to use when dealing with several locks in the same module or package, since all of the locks can be understood by one programmer.  Among locks held by separate modules, as long as there is a standard ordering in which modules call one another, so that for example module foo always calls module bar, while module bar never calls module foo, the locking hierarchy within the individual modules will naturally extend to the set of all modules.
Sometimes getting a simple locking hierarchy is difficult to do, however.  When establishing a good locking hierarchy is difficult or impossible, another possibility is releasing one lock when obtaining another lock.  In the example above, thread 1 might grab lock A, do some work, drop lock A and obtain lock B, do more work, and then drop lock B and grab lock A.  Since it never holds two locks at once, it avoids the deadlock.  However, when it grabs lock A for a second time, it must be prepared to see changes made to the structures protected by lock A, since for a time lock A was not held.  Furthermore, if lock A was a write lock or a mutex, we also must examine all other threads that might have seen the data structure protected by lock A in the state after we released the lock A the first time.  We must ensure that they view the state they might see as a consistent state, and are prepared to deal with the structures in that state. 
Finally, another choice we have is to use the lock_TryReadand similar functions.  Since the trylock functions don't block, we can use them safely to obtain the second through Nth locks in a series.  For example, the sequence:
    lock_ObtainWrite(&lockB); 
    lock_ObtainWrite(&lockA); 

can be replaced by this:
    while (1) { 
        lock_ObtainWrite(&lockB); 
        if (lock_TryWrite(&lockA)) break; 
        
        /* otherwise we failed to get lock A, so camp on it 
         * safely. 
         */ 
        lock_ReleaseWrite(&lockB); 
        lock_ObtainWrite(&lockA); 
        
        /* once we get here, we've gotten lock A.  Hopefully 
         * lock B is now available, too.  Let's quickly drop 
         * this guy and try again. 
         */ 
        lock_ReleaseWrite(&lockA); 
    } 
    
    /* when we get here, we have locked B and then A without having 
     * released B while obtaining A (in the last iteration). 
     */

Clearly the second piece of code is considerably more complex, but it does give the programmer the ability to lock locks out of order, and do computations based on the information seen under those locks.  All modifications, of course, must be delayed until all of the locks have been obtained at the exit of the big while loop, but that is usually not a significant restriction.

#$+The state of any instance of the OSI package can be examined remotely by the osidebug program, if the programmer so chooses.  In order to export the basic information, which consists of information on the thread IDs and sleep values for threads blocked in one of the osi_Sleep functions, or blocked waiting for a lock.  The sleep value of a thread waiting for a lock is the address of the lock.
Furthermore, if lock statistics gathering has been enabled by calling osi_LockTypeSetDefault before initializing some locks, then statistics are gathered on those locks, and the osidebug program will report those statistics, too.
To access any information remotely, the function osi_InitDebug must be called.  If this is not done, then the osidebug program will simply fail to bind to the application program.
If the system deadlocks, enough information may be available from osidebug's output to enable the programmer to figure out what is wrong with the application program without resorting to a debugger on the target machine; this is especially convenient when debugging problems from a remote machine.
More detailed information on osidebug can be obtained from that program's help menu.

#$K+int osi_LockTypeSetDefault(char *name)
This function takes a name of a lock type and sets a global state variable controlling the type of lock initialized by lock_InitializeMutex and lock_InitializeRWLock.  Normally, when a lock initialization function is called, it creates a lock of a specific type, currently, a regular lock or mutex, or a statistics-gathering lock or mutex.  Which type is created is determined by this global variable.
If the name is "stat", then statistics gathering locks and mutexes will be created.  If the type name is passed in as a NULL pointer, regular, non-statistics gathering lock and mutexes will be created.
Since this is a global variable, it controls the type of lock and mutex created by all threads henceforth in this process.
Note that it does not matter what the state of default lock type is when you are gathering statistics.  All that matter is the type of default lock at the time the locks are initialized. 
The system initializes itself creating regular locks and mutexes; this function must be used to enable statistics gathering, and it must be called before the calls to lock_InitializeMutex and lock_InitializeRWLock are done that initialize the locks whose statistics are to be gathered.  After the locks have been initialized, this variable may even be reset, and the initialized locks will continue to gather statistics.