Archive
Lock, Monitor, Mutex, Semaphore
Locking
Exclusive locking is used to ensure that only one thread can enter particular sections of code at a time. The two main exclusive locking constructs are lock
and Mutex
. Of the two, the lock
construct is faster and more convenient.Mutex
, though, has a niche in that its lock can span applications in different processes on the computer.
Let’s start with the following class:
class ThreadUnsafe { static int _val1 = 1, _val2 = 1; static void Go() { if (_val2 != 0) Console.WriteLine (_val1 / _val2); _val2 = 0; } }
This class is not thread-safe: if Go
was called by two threads simultaneously, it would be possible to get a division-by-zero error, because _val2
could be set to zero in one thread right as the other thread was in between executing the if
statement and Console.WriteLine
.
Here’s how lock
can fix the problem:
class ThreadSafe { static readonly object _locker = new object(); static int _val1, _val2; static void Go() { lock (_locker) { if (_val2 != 0) Console.WriteLine (_val1 / _val2); _val2 = 0; } } }
Only one thread can lock the synchronizing object (in this case, _locker
) at a time, and any contending threads areblocked until the lock is released. If more than one thread contends the lock, they are queued on a “ready queue” and granted the lock on a first-come, first-served basis (a caveat is that nuances in the behavior of Windows and the CLR mean that the fairness of the queue can sometimes be violated). Exclusive locks are sometimes said to enforceserialized access to whatever’s protected by the lock, because one thread’s access cannot overlap with that of another. In this case, we’re protecting the logic inside the Go
method, as well as the fields _val1
and _val2
.
Monitor.Enter and Monitor.Exit
C#’s lock
statement is in fact a syntactic shortcut for a call to the methods Monitor.Enter
and Monitor.Exit
, with atry
/finally
block. Here’s (a simplified version of) what’s actually happening within the Go
method of the preceding example:
Monitor.Enter (_locker); try { if (_val2 != 0) Console.WriteLine (_val1 / _val2); _val2 = 0; } finally { Monitor.Exit (_locker); }
Calling Monitor.Exit
without first calling Monitor.Enter
on the same object throws an exception.
Mutex
A Mutex
is like a C# lock
, but it can work across multiple processes. In other words, Mutex
can be computer-wideas well as application-wide.
Acquiring and releasing an uncontended Mutex
takes a few microseconds — about 50 times slower than a lock
.
With a Mutex
class, you call the WaitOne
method to lock and ReleaseMutex
to unlock. Closing or disposing aMutex
automatically releases it. Just as with the lock
statement, a Mutex
can be released only from the same thread that obtained it.
A common use for a cross-process Mutex
is to ensure that only one instance of a program can run at a time. Here’s how it’s done:
class OneAtATimePlease { static void Main() { // Naming a Mutex makes it available computer-wide. Use a name that's // unique to your company and application (e.g., include your URL). using (var mutex = new Mutex (false, "oreilly.com OneAtATimeDemo")) { // Wait a few seconds if contended, in case another instance // of the program is still in the process of shutting down. if (!mutex.WaitOne (TimeSpan.FromSeconds (3), false)) { Console.WriteLine ("Another app instance is running. Bye!"); return; } RunProgram(); } } static void RunProgram() { Console.WriteLine ("Running. Press Enter to exit"); Console.ReadLine(); } }
If running under Terminal Services, a computer-wide Mutex
is ordinarily visible only to applications in the same terminal server session. To make it visible to all terminal server sessions, prefix its name with Global\.
Semaphore
A semaphore is like a nightclub: it has a certain capacity, enforced by a bouncer. Once it’s full, no more people can enter, and a queue builds up outside. Then, for each person that leaves, one person enters from the head of the queue. The constructor requires a minimum of two arguments: the number of places currently available in the nightclub and the club’s total capacity.
A semaphore with a capacity of one is similar to a Mutex
or lock
, except that the semaphore has no “owner” — it’sthread-agnostic. Any thread can call Release
on a Semaphore
, whereas with Mutex
and lock
, only the thread that obtained the lock can release it.
There are two functionally similar versions of this class: Semaphore
and SemaphoreSlim
. The latter was introduced in Framework 4.0 and has been optimized to meet the low-latency demands of parallel programming. It’s also useful in traditional multithreading because it lets you specify acancellation token when waiting. It cannot, however, be used for interprocess signaling.
Semaphore
incurs about 1 microsecond in calling WaitOne
or Release
; SemaphoreSlim
incurs about a quarter of that.
Semaphores can be useful in limiting concurrency — preventing too many threads from executing a particular piece of code at once. In the following example, five threads try to enter a nightclub that allows only three threads in at once:
class TheClub // No door lists! { static SemaphoreSlim _sem = new SemaphoreSlim (3); // Capacity of 3 static void Main() { for (int i = 1; i <= 5; i++) new Thread (Enter).Start (i); } static void Enter (object id) { Console.WriteLine (id + " wants to enter"); _sem.Wait(); Console.WriteLine (id + " is in!"); // Only three threads Thread.Sleep (1000 * (int) id); // can be here at Console.WriteLine (id + " is leaving"); // a time. _sem.Release(); } } 1 wants to enter 1 is in! 2 wants to enter 2 is in! 3 wants to enter 3 is in! 4 wants to enter 5 wants to enter 1 is leaving 4 is in! 2 is leaving 5 is in!
If the Sleep
statement was instead performing intensive disk I/O, the Semaphore
would improve overall performance by limiting excessive concurrent hard-drive activity.
A Semaphore
, if named, can span processes in the same way as a Mutex
.
Recent Comments