Deadlocks
A deadlock is a situation when two or more threads are blocked forever because they depend on each other or a resource that never gets freed up. Let's try to understand how a deadlock occurs by taking a simple example:
Consider our previous example of the JSON to YAML converter. Now, let's assume we had used locks in our threads such that when a thread starts to write to the file, it first takes a mutex lock on the file. Now, until this mutex lock is freed up by the thread, other thread cannot execute.
So, let's imagine the same situation with two threads, writer-1 and writer-2, which are trying to write to the common output file. Now, when writer-1 starts to execute, it first acquires a lock on the file and starts its operation. Now, during the execution of writer-1, a context switch occurs and control transfers to writer-2. Now, writer-2 tries to acquire the lock but enters a blocking state, since writer-1 is currently holding the lock. The control transfers back to the writer-1 thread and it completes its execution, writing all its data. But due to a bug, the writer-1 thread does not release the acquired lock, even though it has completed execution.
This causes the writer-2 thread to stay in the blocking state, since the lock was never freed. A situation such as that experienced by the writer-2 thread, where it stays in a blocking state, is also known as a deadlock.
Deadlocks can happen when an acquired lock is not freed up properly or if a thread doesn't have a mechanism to time out if lock acquisition takes a long time. So, let's see some of the ways through which we can avoid deadlocks occurring:
- Taking care to release the acquired locks: If the programmer is careful about releasing the acquired locks by a thread, then deadlocks can be avoided quite easily. This can be achieved by calling the associated release call of an acquired lock and implementing proper thread cleanup mechanisms if the thread terminates abruptly during its execution.
- Specifying lock acquisition timeout: One other way to prevent deadlocks is to specify a timeout value for which a particular thread will wait to acquire the lock. If this timeout value is exceeded, the acquisition operations execution will fail with an exception. In Python, the timeout value for a lock acquisition can be provided as a parameter to the acquire()method. This can be specified as: acquire(timeout=10). Here, the timeout parameter specifies the time in seconds for which the acquire operation will wait for the lock to be acquired and will fail if the lock is not acquired during the specified duration, essentially avoiding the thread being in a blocked state forever.
With this, we now have knowledge about two common pitfalls that can happen in multithreaded applications and how they can be avoided. Now, let's take the opportunity to discuss a common design pitfall with some specific interpreters of Python and how it can affect multithreaded operations.