mutual exclusion (mutex)
What is a mutual exclusion (mutex)?
In computer programming, a mutual exclusion (mutex) is a program object that prevents multiple threads from accessing the same shared resource simultaneously. A shared resource in this context is a code element with a critical section, the part of the code that should not be executed by more than one thread at a time. For example, a critical section might update a global variable, modify a table in a database or write a file to a network server. In such cases, access to the shared resource must be controlled to prevent problems to the data or the program itself.
What is a mutex object?
In a multithreaded program, a mutex is a mechanism used to ensure that multiple concurrent threads do not try to execute a critical section of code simultaneously. If a mutex is not applied, the program might be subject to a race condition, a situation in which multiple threads try to access a shared resource at the same time. When this happens, unintended results can occur, such as data being read or written incorrectly or the program misbehaving or crashing.
To understand how a mutex works, consider a multithreaded program that contains a method for updating a variable used as a counter. The method, which contains the critical section, is responsible for reading the variable's current value, incrementing that value by 1 and writing the new value to memory. If two threads run the method one at a time in consecutive order, the first thread will add 1 to the original value -- 1 in this case -- resulting in a new value of 2, which is written to memory. The second thread then reads the value of 2 and increases it by 1, resulting in a new value of 3. Figure 1 shows how this process works.
Unfortunately, multithreaded programs do not always execute code in such a linear fashion, and it is possible for different threads to step over each other as they execute the code. In this case, the first thread might read the variable value, followed immediately by the second thread reading the same value, in which case they're both starting with the value 1, as shown in Figure 2. When this occurs, the final variable value likely ends up being 2 instead of the expected 3.
In a multithreaded program, a mutex ensures that the threads carry out their operations like the two threads shown in Figure 1, with each thread accessing the critical section of code one at a time. This way, they don't step over each other and return undesired results, but instead behave in a controlled and predictable manner.
Mutex in programming
A multithreaded program can specifically request a mutex for each shared resource from the underlying system. If a thread needs to access the resource, it must first verify whether the mutex for that resource is locked. If it is unlocked, the thread can execute the code's critical section. If it is locked, the system typically queues the thread until the mutex becomes unlocked. When this occurs, the thread can execute the critical section, while the mutex is again locked to prevent other threads from using the resource.
The exact approach to request a mutex lock depends on the programming language. In Python, for example, the threading module can be imported, which enables a lock for a shared resource to be acquired. The following Python script demonstrates how this works:
import threading
import time
mutex = threading.Lock()
def get_data(cnt):
mutex.acquire()
try:
thread_id = threading.get_ident()
time.sleep(0.5)
print(f"Count: {cnt} (thread {thread_id})")
finally:
mutex.release()
count = 1
max_count = 10
while True:
thread = threading.Thread(target=get_data, args=(count,))
thread.start()
count = count + 1
if count > max_count:
break
The script creates a Lock object from the threading module and assigns it to the mutex variable. It then defines the get_data function. The function starts by using the acquire method on the Lock object to request the lock, and it ends by calling the release method, which releases the lock after the function carries out its operations. Before releasing the lock, the function retrieves the current thread ID, adds a half-second delay -- using the time.sleep method -- and then prints the cnt parameter value and the thread ID.
The function definition is then followed by a while loop that creates a Thread object for each iteration, assigning the object to the thread variable. The thread object runs the get_data function, supplying a value for the cnt parameter. Each iteration also uses the start method to launch the thread. It then increments the count variable by 1. The script returns the following results:
Count: 1 (thread 6183661568)
Count: 2 (thread 6200487936)
Count: 3 (thread 6217314304)
Count: 4 (thread 6234140672)
Count: 5 (thread 6250967040)
Count: 6 (thread 6267793408)
Count: 7 (thread 6284619776)
Count: 8 (thread 6301446144)
Count: 9 (thread 6318272512)
Count: 10 (thread 6335098880)
The thread IDs returned by the script are specific to the system on which the script ran. Notice that the results are returned in the order in which each thread is executed, as evidenced by the consecutive count values. Because the function acquires a mutex lock each time it runs, no other active threads can execute the function until the previous thread has released its lock.
This is a rudimentary example of a mutex in action that demonstrates the basic idea behind how a mutex protects a shared resource. Without it, the results could be much different. For example, the following script is similar to the previous one, except that it does not acquire a mutex lock:
import threading
import time
def get_data(cnt):
time.sleep(0.5)
thread_id = threading.get_ident()
print(f"Count: {cnt} (thread {thread_id})")
count = 1
max_count = 10
while True:
thread = threading.Thread(target=get_data, args=(count,))
thread.start()
count = count + 1
if count > max_count:
break
Once again, the script includes a while loop that launches a thread with each iteration, with the Thread object invoking the get_data function. However, because the function does not acquire a mutex lock, the results are much less orderly:
Count: 4 (thread 6161756160)
Count: 7 (thread 6212235264)
Count: 8 (thread 6229061632)
Count: 1 (thread 6111277056)Count: 3 (thread 6144929792)Count: 5 (thread 6178582528)
Count: 10 (thread 6262714368)Count: 6 (thread 6195408896)
Count: 2 (thread 6128103424)
Count: 9 (thread 6245888000)
The threads now run in a haphazard manner, essentially walking all over each other. In addition, each time the script is run, it is likely to return different results. Without the mutex, the data becomes much less reliable and the outcome less consistent or predictable, undermining the program's viability.
See in-demand programming languages devs should get to know.