Basic Concept

If a data item is shared by a number threads, race conditions could occur if the shared item is not protected properly. The easiest protection mechanism is a lock. In general, if a set of data items must be protected so that at any time there is no more than one thread can have access to it, we can associate the set of data items with a lock. The use of locks is actually very easy. For every thread, before it accesses the set of data items, it acquires the lock. Once the lock is successfully acquired, the thread becomes the owner of that lock and the lock is locked. Then, the owner can access the protected items. After this, the owner must release the lock and the lock becomes unlocked. It is possible that while the owner is accessing one of the protected data items and another thread comes. Of course, this second thread must acquire that lock. However, since the lock is locked, this request is unsuccessful and the requesting thread will be suspended and queued at the lock. When the lock is released by its owner, one of the waiting threads will be allowed to continue and locks the lock.

This mechanism can be seen in everyday life. For example, on an airplane, before you use its lavatory, you check to see if it is locked. If it is, you join the waiting line; otherwise, you enter and lock the door. Once the door is locked, you are protected from the intrusion of anybody else (i.e., mutual exclusion). When you exit, you unlock the door so that one of those waiting can enter. There could be more than one waiting persons, and who will be allowed to enter and lock the door depends on some queuing policy (e.g., first-in-first-out). But, a good programmer should not make any assumption about this policy.

Therefore, the use of a lock simply establishes a critical section as shown below. Before entering a critical section, a thread acquires a lock. If it is successful, this thread enters the critical section and the lock is locked. As a result, all subsequent acquiring requests will be queued until the lock is unlocked. In this way, the owner of the lock (i.e., the thread that successfully acquired the lock) is the only thread that can execution the instructions of the indicated critical section. At the end of the execution of the instructions in a critical section, the owner releases the lock, and, at this point, the lock is unlocked, allowing the next thread to enter this critical section. Therefore, mutual exclusion is guaranteed. Because of this, a lock is also usually referred to as a mutex for mutual exclusion.

In general, there are a number of restrictions to the use of locks, although not all systems enforce the same set of restrictions. However, it would be very helpful for a programmer to know the possible restrictions. The first restriction is only the owner can release the lock. This is a very natural requirement. Imagine the following situation. Suppose thread A is the current owner of lock L and thread B is a second thread who wants to lock the lock. If a non-owner can unlock a lock, thread B can unlock the lock that thread A owns, and, hence, either both threads may be executing in the same critical section, or thread B preempts thread A and executes the instructions of the critical section. However, both are not very secured ways of protecting the shared items. Thus, in most systems, ThreadMentor included, only the owner of a lock can release the lock.

The second restriction is recursive lock acquisition is not allowed. This means the current owner of the lock is not allowed to acquire the same lock again. More precisely, if thread A currently owns lock L. If thread A wants to own lock L again, it must release lock L and re-acquire lock it again. Some systems permit a lock to be acquired recursively because this is useful in some applications; however, ThreadMentor does not allow this to happen because we believe that a programmer must know all the fundamentals before s/he starts to do something strange such as acquiring the same lock recursively.

An Important Note

All threads that use the protected data items must use the same protocol to acquire and release the associated lock. Otherwise, mutual exclusion will not be guaranteed. More precisely, if three threads A, B and C share the same set of data items, and if only threads A and B acquire and release the associated lock, then while thread A is executing the instructions of the critical section and thread B is waiting outside, thread C can enter the critical section because it does not have to wait until thread A releases the lock. As a result, a race condition occurs and the final result of the computation may be incorrect.