Locks
A lock is an abstract concept. It protects access to some kind of shared resource. A thread owning a lock has access to the protected shared resource. In simple words whatever code is enclosed in lock scope only one thread can enter in that lock scope and other threads have to wait till entered thread completes its work/ executes its work.
Locks/Monitors ensures the thread safety which are in process that is threads which are generated by an application (Internal threads) it does not have any control over the threads which are coming from outside of an application. Mutex ensures the thread safety which are out process i.e. threads which coming from outside of an application (external threads).
Mutex
Mutex is short for MUTual EXclusion. Unless the word is qualified with additional terms such as shared mutex, recursive mutex or read/write mutex then it refers to a type of lockable object that can be owned by exactly one thread at a time. Only the thread that acquired the lock can release the lock on a mutex. When the mutex is locked, any attempt to acquire the lock will fail or block, even if that attempt is done by the same thread.
Recursive mutex is similar to a plain mutex, but one thread may own multiple locks on it at the same time. If a lock on a recursive mutex has been acquired by thread A, then thread A can acquire further locks on the recursive mutex without releasing the locks already held. However, thread B cannot acquire any locks on the recursive mutex until all the locks held by thread A have been released.
At the lowest level, mutex use some sort of atomic test and set mechanism. This reads the current value of a memory location, computes some sort of conditional and writes out a value at that location in a single instruction that cannot be interrupted.
When a thread finished executing protected code (known as a Critical Section), it set the mutex value to zero or whatever means ‘clear.’ If multiple threads are attempting to acquire the mutex, next thread that happens to be scheduled after the mutex is released will get access to the resource. They are used to control a synchronised resource where exclusive access is only needed for very short periods of time, normally to make an update to a shared data structure.
Spinlock
When regular locks (mutexes, critical sections etc) are used, operating system puts thread in the WAIT state and preempts it by scheduling other threads on the same core. This has a performance penalty, because thread now has to wait for a preemption to receive CPU time again. Spinlocks don’t cause preemption but wait in a loop (“Spin”) till the other core releases the lock while repeatedly checking if the lock is available. This prevents the thread from losing its quantum and continue as soon as the lock gets released. In a loop a thread waits simply (‘spins’) checks repeatedly until the lock becomes available.
Spinlock is a kind of busy waiting, as the threads remains active by not performing a useful task. Once acquired, spinlocks will usually be held until they are explicitly released.
Semaphores
A semaphore are integer variables for which only two (atomic) operations are defined the wait and signal operations A thread take ownership of a semaphore with a wait() operation, also referred to as decrementing the semaphore. A thread release ownership with a signal() operation, also referred to as incrementing the semaphore, a post operation.
The wait() and signal() operation modify the value of the semaphore indivisibly. It means that when a process is modifying the value of the semaphore, no other process can simultaneously modify the value of the semaphore. In Counting Semaphore, the semaphore S value is initialized to the number of resources present in the system. Whenever a process wants to access the resource, it performs wait() operation on the semaphore and decrements the value of semaphore by one. When it releases the resource, it performs signal() operation on the semaphore and increments the value of semaphore by one. When the semaphore count goes to 0, it means all resources are occupied by the processes. If a process need to use a resource when semaphore count is 0, it executes wait() and get blocked until the value of semaphore becomes greater than 0.
Binary semaphore can only be either 0 or 1. They are also known as mutex locks, as the locks can provide mutual exclusion. All the processes can share the same mutex semaphore that is initialized to 1. Then, a process has to wait until the lock becomes 0. Then, the process can make the mutex semaphore 1 and start its critical section. When it completes its critical section, it can reset the value of mutex semaphore to 0 and some other process can enter its critical section.
The key idea is that there is a resource that can be given and taken by different processes. The purpose is to have a mechanism for a process to notify some other process that a resource is available now. For example, one process is waiting for a signal to start processing data that another process puts into a data structure. The other process puts the data for processing and increments the semaphore. The first process now starts processing that data and decrements the semaphore.
Mutex Vs Semaphore
- Mutex is locking mechanism used to synchronize access to a resource. Semaphore is signaling mechanism.
- A mutex is used to meet the atomicity requirement. It does not impose any ordering. Given two threads, mutex can’t specify, which thread will acquire the mutex first. On the other hand, a semaphore can be used to impose ordering constraints in execution. Considering the example, thread T2 wait() on the same condition variable that T1 signal().
- A mutex can be released only by thread that had acquired it. While a thread can signal semaphore from any other thread (or process).
- The mutex is similar to the principles of the binary semaphore with one significant difference. If a thread tries to unlock a mutex, it has not locked then an error condition is encountered and, the mutex is not unlocked.
Spinlock Vs Mutex
Putting thread to sleep and waking them up again are expensive operations, they’ll need quite a lot of CPU instructions. If mutex was only locked for a very short amount of time, the time spent in putting a thread to sleep and waking it up again might exceed the time thread would have wasted by constantly polling on a spinlock. On the other hand, polling on a spinlock will constantly waste CPU time and if the lock is held for a longer amount of time, this will waste a lot more CPU time and it would have been much better if the thread was sleeping instead.
Using spinlocks on a single-core/single-CPU system makes usually no sense, since as long as the spinlock polling is blocking the only available CPU core, no other thread can run and since no other thread can run, the lock won’t be unlocked either. On a multi-core/multi-CPU systems, with plenty of locks that are held for a very short amount of time only, the time wasted for constantly putting threads to sleep and waking them up again might decrease runtime performance noticeably. When using spinlocks instead, threads get the chance to take advantage of their full runtime quantum (always only blocking for a very short time period, but then immediately continue their work), leading to much higher processing throughput.
If in doubt, use mutexes, they are usually the better choice and most modern systems will allow them to spinlock for a very short amount of time, if this seems beneficial. Using spinlocks can sometimes improve performance, but only under certain conditions and the fact that you are in doubt rather tells me, that you are not working on any project currently where a spinlock might be beneficial.
Example
Consider the standard producer-consumer problem. Assume, with a buffer of 4096 byte length. A producer thread collects the data and writes it to the buffer. A consumer thread processes the collected data from the buffer. Objective is, both the threads should not run at the same time.
A mutex provides mutual exclusion, either producer or consumer can have the key (mutex) and proceed with their work. As long as the buffer is filled by producer, the consumer needs to wait, and vice versa. At any point of time, only one thread can work with the entire buffer.
Split single 4 KB buffer into four 1 KB buffers (identical resources). A semaphore can be associated with these four buffers. The consumer and producer can work on different buffers at the same time.