Lock, mutex, semaphore... what's the difference?
A lock allows only one thread to enter the part that's locked and the lock is not shared with any other processes.
A mutex is the same as a lock but it can be system wide (shared by multiple processes).
A semaphore does the same as a mutex but allows x number of threads to enter, this can be used for example to limit the number of cpu, io or ram intensive tasks running at the same time.
For a more detailed post about the differences between mutex and semaphore read here.
You also have read/write locks that allows either unlimited number of readers or 1 writer at any given time.
There are a lot of misconceptions regarding these words.
This is from a previous post (https://stackoverflow.com/a/24582076/3163691) which fits superb here:
1) Critical Section= User object used for allowing the execution of just one active thread from many others within one process. The other non selected threads (@ acquiring this object) are put to sleep.
[No interprocess capability, very primitive object].
2) Mutex Semaphore (aka Mutex)= Kernel object used for allowing the execution of just one active thread from many others, among different processes. The other non selected threads (@ acquiring this object) are put to sleep. This object supports thread ownership, thread termination notification, recursion (multiple 'acquire' calls from same thread) and 'priority inversion avoidance'.
[Interprocess capability, very safe to use, a kind of 'high level' synchronization object].
3) Counting Semaphore (aka Semaphore)= Kernel object used for allowing the execution of a group of active threads from many others. The other non selected threads (@ acquiring this object) are put to sleep.
[Interprocess capability however not very safe to use because it lacks following 'mutex' attributes: thread termination notification, recursion?, 'priority inversion avoidance'?, etc].
4) And now, talking about 'spinlocks', first some definitions:
Critical Region= A region of memory shared by 2 or more processes.
Lock= A variable whose value allows or denies the entrance to a 'critical region'. (It could be implemented as a simple 'boolean flag').
Busy waiting= Continuosly testing of a variable until some value appears.
Finally:
Spin-lock (aka Spinlock)= A lock which uses busy waiting. (The acquiring of the lock is made by xchg or similar atomic operations).
[No thread sleeping, mostly used at kernel level only. Ineffcient for User level code].
As a last comment, I am not sure but I can bet you some big bucks that the above first 3 synchronizing objects (#1, #2 and #3) make use of this simple beast (#4) as part of their implementation.
Have a good day!.
References:
-Real-Time Concepts for Embedded Systems by Qing Li with Caroline Yao (CMP Books).
-Modern Operating Systems (3rd) by Andrew Tanenbaum (Pearson Education International).
-Programming Applications for Microsoft Windows (4th) by Jeffrey Richter (Microsoft Programming Series).
Also, you can take a look at look at: https://stackoverflow.com/a/24586803/3163691
Most problems can be solved using (i) just locks, (ii) just semaphores, ..., or (iii) a combination of both! As you may have discovered, they're very similar: both prevent race conditions, both have acquire()
/release()
operations, both cause zero or more threads to become blocked/suspected...
Really, the crucial difference lies solely on how they lock and unlock.
- A lock (or mutex) has two states (0 or 1). It can be either unlocked or locked. They're often used to ensure only one thread enters a critical section at a time.
- A semaphore has many states (0, 1, 2, ...). It can be locked (state 0) or unlocked (states 1, 2, 3, ...). One or more semaphores are often used together to ensure that only one thread enters a critical section precisely when the number of units of some resource has/hasn't reached a particular value (either via counting down to that value or counting up to that value).
For both locks/semaphores, trying to call acquire()
while the primitive is in state 0 causes the invoking thread to be suspended. For locks - attempts to acquire the lock is in state 1 are successful. For semaphores - attempts to acquire the lock in states {1, 2, 3, ...} are successful.
For locks in state state 0, if same thread that had previously called acquire()
, now calls release, then the release is successful. If a different thread tried this -- it is down to the implementation/library as to what happens (usually the attempt ignored or an error is thrown). For semaphores in state 0, any thread can call release and it will be successful (regardless of which thread previous used acquire to put the semaphore in state 0).
From the preceding discussion, we can see that locks have a notion of an owner (the sole thread that can call release is the owner), whereas semaphores do not have an owner (any thread can call release on a semaphore).
What causes a lot of confusion is that, in practice they are many variations of this high-level definition.
Important variations to consider:
- What should the
acquire()
/release()
be called? -- [Varies massively] - Does your lock/semaphore use a "queue" or a "set" to remember the threads waiting?
- Can your lock/semaphore be shared with threads of other processes?
- Is your lock "reentrant"? -- [Usually yes].
- Is your lock "blocking/non-blocking"? -- [Normally non-blocking are used as blocking locks (aka spin-locks) cause busy waiting].
- How do you ensure the operations are "atomic"?
These depends on your book / lecturer / language / library / environment.
Here's a quick tour noting how some languages answer these details.
C, C++ (pthreads)
- A mutex is implemented via
pthread_mutex_t
. By default, they can't be shared with any other processes (PTHREAD_PROCESS_PRIVATE
), however mutex's have an attribute called pshared. When set, so the mutex is shared between processes (PTHREAD_PROCESS_SHARED
). - A lock is the same thing as a mutex.
- A semaphore is implemented via
sem_t
. Similar to mutexes, semaphores can be shared between threasds of many processes or kept private to the threads of one single process. This depends on the pshared argument provided tosem_init
.
python (threading.py)
- A lock (
threading.RLock
) is mostly the same as C/C++pthread_mutex_t
s. Both are both reentrant. This means they may only be unlocked by the same thread that locked it. It is the case thatsem_t
semaphores,threading.Semaphore
semaphores andtheading.Lock
locks are not reentrant -- for it is the case any thread can perform unlock the lock / down the semaphore. - A mutex is the same as a lock (the term is not used often in python).
- A semaphore (
threading.Semaphore
) is mostly the same assem_t
. Although withsem_t
, a queue of thread ids is used to remember the order in which threads became blocked when attempting to lock it while it is locked. When a thread unlocks a semaphore, the first thread in the queue (if there is one) is chosen to be the new owner. The thread identifier is taken off the queue and the semaphore becomes locked again. However, withthreading.Semaphore
, a set is used instead of a queue, so the order in which threads became blocked is not stored -- any thread in the set may be chosen to be the next owner.
Java (java.util.concurrent)
- A lock (
java.util.concurrent.ReentrantLock
) is mostly the same as C/C++pthread_mutex_t
's, and Python'sthreading.RLock
in that it also implements a reentrant lock. Sharing locks between processes is harder in Java because of the JVM acting as an intermediary. If a thread tries to unlock a lock it doesn't own, anIllegalMonitorStateException
is thrown. - A mutex is the same as a lock (the term is not used often in Java).
- A semaphore (
java.util.concurrent.Semaphore
) is mostly the same assem_t
andthreading.Semaphore
. The constructor for Java semaphores accept a fairness boolean parameter that control whether to use a set (false) or a queue (true) for storing the waiting threads.
In theory, semaphores are often discussed, but in practice, semaphores aren't used so much. A semaphore only hold the state of one integer, so often it's rather inflexible and many are needed at once -- causing difficulty in understanding code. Also, the fact that any thread can release a semaphore is sometimes undesired. More object-oriented / higher-level synchronization primitives / abstractions such as "condition variables" and "monitors" are used instead.