Memory Management: 3.6 Using Global Memory: Data-Access Synchronization

Up: GEOS SDK TechDocs | Up | Prev: 3.5 Accessing Data: An Example | Next: 3.7 Retrieving Block Information
MemThreadGrab(), MemThreadGrabNB(), MemThreadRelease(), MemLockShared(), MemUnlockShared(), MemLockExcl(), MemDowngradeExclLock(), MemUpgradeSharedLock(), MemUnlockExcl(), HandleP(), HandleV(), MemPLock(), MemUnlockV()

Blocks can have the property of being sharable--that is, the same block may be locked by threads owned by several different geodes. However, this can cause data synchronization problems; one application can be changing data while another application is trying to use it. To prevent this, GEOS provides semaphore routines. Only one thread can have the block's semaphore at a time. When an application wants to use a shared block, it should call a routine to set the semaphore. Once the routine returns, the application can use the block; when it is done, it should release the block's semaphore so other applications can use the block.

Note that use of semaphores is entirely voluntary by each application. Even if thread A has the semaphore on a block, thread B can call MemLock() on the block and start changing it. However, all threads using shared blocks ought to use the semaphore routines to prevent confusion.

There are several different sets of routines which can be used to control a block's semaphore. The different sets of routines make different trade-offs between faster operation and increased flexibility. Any one block should be accessed with only one set of routines; different threads should all be using the same routines to access a given block, and a thread should not switch from one set of routines to another for a particular block. If this rule isn't followed, results are undefined. All of these routines access the HM _otherInfo word of the handle table entry; if the block will be locked by any of these routines, you must not alter that word. None of these routines is used to access object blocks; instead, special object-block locking routines are provided.

Most geodes should use MemThreadGrab() , MemThreadGrabNB() , and MemThreadRelease() to access sharable blocks. These routines provide the maximum protection against deadlock in exchange for a slightly slower execution.

MemThreadGrab() gives the thread the semaphore for the block in question and locks the block. It is passed the handle of the block and returns the block's address on the global heap. If no thread has the block's semaphore, it gives the semaphore to the calling thread. If the calling thread already has the semaphore, a "semaphore count" is incremented; the thread will not release the semaphore until it has been released as many times as it has been grabbed. (For example, two different objects run by the same thread could each grab the semaphore; the semaphore would not be released until each object called MemThreadRelease() .) If another thread has the semaphore, MemThreadGrab() blocks until it can get the semaphore; it then increments the semaphore, locks the block, and returns the address.

MemThreadGrabNB() is the same as MemThreadGrab() , except that it never blocks. If you call MemThreadGrabNB() while another thread has the semaphore, the routine will immediately return an error. MemThreadGrabNB() takes the handle of the block; it increments the semaphore, locks the block, and returns the block's address on the heap.

MemThreadRelease() releases a block grabbed by either MemThreadGrab() or MemThreadGrabNB() . It is passed the block's handle. It unlocks the block and decrements the block's semaphore.

One common situation is that several threads may need to read a block but only once in a while will an application need to write to the block. In this case, there is no synchronization problem in having several readers at once; however, if any thread is writing, no other thread should be reading or writing. For this situation, GEOS provides this set of MemLock routines: MemLockShared() , MemUnlockShared() , MemLockExcl() , and MemUnlockExcl() .

These routines, like the others, maintain a queue of threads which have requested thread access. The difference is that any number of readers can have access at once. When a thread wants read access, it calls MemLockShared() . If the queue is empty and the block is unlocked or locked for reading, the routine returns and the thread is given shared access; otherwise, the thread is blocked, and the request goes on the queue. When a routine is finished reading the block, it should call MemUnlockShared() .

When a routine needs to write to a block, it should call MemLockExcl() . If nobody has locked the block (and thus the queue is empty), the thread will immediately be given exclusive access; otherwise, the thread will block, and the request will go on the queue. When the thread no longer needs write access, it should call MemUnlockExcl() .

When all threads with access to a block have released their locks, the queued thread with the highest priority will be awakened and given the lock on the block. If that thread had requested shared access, all other threads on the queue that had requested shared access will also be awakened and given locks.

A thread can change its lock from shared to exclusive or vice versa. If a thread has an exclusive lock on a block, it can change the lock to shared by calling MemDowngradeExclLock() . This routine takes one argument, namely the block's global handle. It changes the lock to "shared" and wakes up all sleeping threads which are waiting for shared access. For convenience, MemDowngradeExclLock() returns the address of the block; however, the block is guaranteed not to move.

If a thread has shared access and wants exclusive access, it can call MemUpgradeSharedLock() . If the thread has the only lock on the block, its lock will be changed to "exclusive" (even if there are writers on the queue). If any other threads have the block locked, the routine will release the thread's lock and put the thread on the queue. When the thread comes to the head of the queue, the routine will wake the thread and give it an exclusive lock. The routine returns the block's address on the global heap. Note that the block may be altered or moved during this call if the call blocked.

Once a thread has been given a shared lock, there is nothing to prevent it from altering (or even freeing) the block. The routines rely on good citizenship by the threads using them.Also, if a thread isn't careful, there is a great danger of deadlock. If (for example) a thread requests exclusive access to a block when it already has access, the thread will deadlock: it will block until the threads with access all release the block, but it can't release its own lock because it is blocked. If you may need to have multiple locks on a block, use the MemThread routines, which check for these situations.

There are other sets of routines which can be used to access a block's semaphore. As noted, a block should be accessed via just one set of routines. These routines provide less protection against deadlock than the MemThread routines do; however, they have a slightly faster response time.

A more primitive group of routines is HandleP() , HandleV() , MemPLock() , and MemUnlockV() . These routines function much like the MemThread routines. HandleP() grabs the block's semaphore and returns; it does not lock the block. This makes it very useful for working with fixed blocks (which cannot be locked). HandleV() releases the block's semaphore and returns; it does not unlock the block. Note, however, that HandleP() will block if any thread controls the semaphore, even the thread that called HandleP() . If a thread calls HandleP() while it controls the semaphore, it will block until the semaphore is released, but it can't release the semaphore because it has blocked. Thus, the thread will deadlock, and no other thread will be able to get the semaphore. Therefore, a thread should use HandleP() only if it is very confident that it will never try to double-set the semaphore.

Usually, when a thread grabs a block's semaphore, it needs to have the block locked on the heap. For this reason, GEOS provides the routines MemPLock() and MemUnlockV() . MemPLock() simply calls HandleP() and then calls MemLock() . MemUnlockV() , correspondingly, calls MemUnlock() and then calls HandleV() . These routines are completely compatible with HandleP() and HandleV() ; for example, a thread could grab and lock a block with MemPLock() , then unlock it with MemUnlock() and release it with HandleV() .

HandleP() and HandleV() are general-purpose handle routines. They can be called on any type of global handle. For example, if two threads need to synchronize their access to a file, they can lock and unlock the file handle with HandleP() and HandleV() . However, they are most commonly used with memory blocks.


Up: GEOS SDK TechDocs | Up | Prev: 3.5 Accessing Data: An Example | Next: 3.7 Retrieving Block Information