Both can limit the number of threads that simultaneously access a specific resource or pool of resources.
Let’s not get theoretical here. We will start with a practical case and gradually delve deeper through example code.
Semaphore Class
Here, we first list the commonly used APIs of the Semaphore class.
Its constructors are as follows:
| Constructor | Description |
|--------------|-------------|
| Semaphore(Int32, Int32) | Initializes a new instance of the Semaphore class, specifying the initial number of entries and the maximum concurrent entries. |
| Semaphore(Int32, Int32, String) | Initializes a new instance of the Semaphore class, specifying the initial number of entries and the maximum concurrent entries, while optionally specifying the name of the system semaphore object. |
| Semaphore(Int32, Int32, String, Boolean) | Initializes a new instance of the Semaphore class, specifying the initial number of entries and the maximum concurrent entries, with an optional name for the system semaphore object and a variable to receive a value indicating whether a new system semaphore was created. |
Semaphore uses a purely kernel-time way (with very short wait times) and supports synchronizing threads across different processes (like Mutex).
Common methods of Semaphore are as follows:
| Method | Description |
|--------|-------------|
| Close() | Releases all resources used by the current WaitHandle. |
| OpenExisting(String) | Opens a semaphore with the specified name (if it already exists). |
| Release() | Exits the semaphore and returns the previous count. |
| Release(Int32) | Exits the semaphore a specified number of times and returns the previous count. |
| TryOpenExisting(String, Semaphore) | Opens a semaphore with the specified name (if it already exists) and returns a value indicating whether the operation was successful. |
| WaitOne() | Blocks the current thread until the current WaitHandle receives a signal. |
| WaitOne(Int32) | Blocks the current thread until the current WaitHandle receives a signal, while using a 32-bit signed integer to specify the time interval (in milliseconds). |
| WaitOne(Int32, Boolean) | Blocks the current thread until the current WaitHandle receives a signal, using a 32-bit signed integer to specify the time interval, and indicates whether to exit the synchronization domain before waiting. |
| WaitOne(TimeSpan) | Blocks the current thread until the current instance receives a signal, using TimeSpan to specify the time interval. |
| WaitOne(TimeSpan, Boolean) | Blocks the current thread until the current instance receives a signal, using TimeSpan to specify the time interval, and indicates whether to exit the synchronization domain before waiting. |
Example
Let’s write some code directly. Here, we use an example from "Atomic Operations Interlocked," where we require multiple threads to perform calculations but only allow up to three threads to execute simultaneously.
Using Semaphore, there are four steps:
- Instantiate the Semaphore and set the maximum threads and initial entry threads.
- Use
.WaitOne();
to obtain entry permission (the thread is blocked until entry permission is granted). - Release the occupied entry using
Release()
. - Use
Close()
to release the Semaphore object.
The improved example from "Atomic Operations Interlocked" is as follows:
class Program
{
// Sum
private static int sum = 0;
private static Semaphore _pool;
// Check if ten threads have completed.
private static int isComplete = 0;
// First program
static void Main(string[] args)
{
Console.WriteLine("Executing program");
// Set maximum three threads allowed in the resource pool
// Initially set to 0, allowing how many threads at initialization
// Here it is set to 0, later when the button is pressed, it can release three threads
_pool = new Semaphore(0, 3);
for (int i = 0; i < 10; i++)
{
new Thread(AddOne).Start(i);
}
// Wait until ten threads complete
while (true)
{
if (Interlocked.CompareExchange(ref isComplete, 0, 0) == 10)
break;
Thread.Sleep(TimeSpan.FromSeconds(1));
}
Console.WriteLine("Sum = " + sum);
// Release pool
_pool.Close();
}
public static void AddOne(object n)
{
Console.WriteLine($"Thread {(int)n} started, entering queue");
// Enter queue waiting
_pool.WaitOne();
Console.WriteLine($"Thread {(int)n} entered resource pool");
// Enter resource pool
for (int i = 0; i < 1000; i++)
{
Interlocked.Add(ref sum, i);
}
Console.WriteLine($"Thread {(int)n} exiting resource pool");
// Exit resource pool
_pool.Release();
}
}
It looks like there’s a lot of code. Go ahead and run it to see the results.
Explanation of Example
The instantiation of Semaphore uses new Semaphore(0, 3);
, with the constructor prototype as follows:
public Semaphore(int initialCount, int maximumCount);
initialCount
indicates how many processes are allowed to enter the resource pool at the start. If set to 0, all threads cannot enter and must wait for access to the resource pool.
maximumCount
indicates the maximum number of threads allowed to enter the resource pool.
Release()
indicates exiting the semaphore and returning the previous count, which reflects how many more threads can enter the resource pool.
You can look at the following example:
private static Semaphore _pool;
static void Main(string[] args)
{
_pool = new Semaphore(0, 5);
_pool.Release(5);
new Thread(AddOne).Start();
Thread.Sleep(TimeSpan.FromSeconds(10));
_pool.Close();
}
public static void AddOne()
{
_pool.WaitOne();
Thread.Sleep(1000);
int count = _pool.Release();
Console.WriteLine("Before this thread exits the resource pool, how many threads can still enter the resource pool? " + count);
}
Semaphore
Previously, we learned about Mutex, which operates globally within the operating system. From Mutex and Semaphore, we see the concept of semaphore.
There are two types of semaphores: local semaphores and named system semaphores.
- Named system semaphores are visible across the entire operating system and can be used to synchronize process activities.
- Local semaphores exist only within the process.
When name
is null or empty, the semaphore of Mutex is a local semaphore; otherwise, the semaphore of Mutex is a named system semaphore.
Semaphore also presents these two cases.
If a Semaphore object is created using the constructor that accepts a name, it will be associated with the operating system semaphore of that name.
Two constructors are:
Semaphore(Int32, Int32, String)
Semaphore(Int32, Int32, String, Boolean)
The constructors above can create multiple Semaphore objects representing the same named system semaphore and can use the OpenExisting method to open an existing named system semaphore.
The example we used above is a local semaphore, where all threads referencing the local Semaphore object within the process can be used. Each Semaphore object is a separate local semaphore.
SemaphoreSlim Class
What is the relationship between SemaphoreSlim and Semaphore?
Let me take a look in the book.
Oh, according to the Microsoft documentation:
SemaphoreSlim represents a lightweight alternative to Semaphore that limits the number of threads that can access resources or pools concurrently.
SemaphoreSlim does not use signaling and does not support inter-process synchronization; it can only be used within a process.
It has two constructors:
| Constructor | Description |
|--------------|-------------|
| SemaphoreSlim(Int32) | Initializes a new instance of the SemaphoreSlim class, specifying the initial number of requests that can be granted concurrently. |
| SemaphoreSlim(Int32, Int32) | Initializes a new instance of the SemaphoreSlim class, specifying both the initial number of requests that can be granted concurrently and the maximum number. |
Example
Let’s modify the previous Semaphore example:
class Program
{
// Sum
private static int sum = 0;
private static SemaphoreSlim _pool;
// Check if ten threads have completed.
private static int isComplete = 0;
static void Main(string[] args)
{
Console.WriteLine("Executing program");
// Set maximum three threads allowed in the resource pool
// Initially set to 0, allowing how many threads at initialization
// Here it is set to 0, later when the button is pressed, it can release three threads
_pool = new SemaphoreSlim(0, 3);
for (int i = 0; i < 10; i++)
{
new Thread(AddOne).Start(i);
}
// Wait until ten threads complete
while (true)
{
if (Interlocked.CompareExchange(ref isComplete, 0, 0) == 10)
break;
Thread.Sleep(TimeSpan.FromSeconds(1));
}
Console.WriteLine("Sum = " + sum);
}
public static void AddOne(object n)
{
Console.WriteLine($"Thread {(int)n} started, entering queue");
// Enter queue waiting
_pool.Wait();
Console.WriteLine($"Thread {(int)n} entered resource pool");
// Enter resource pool
for (int i = 0; i < 1000; i++)
{
Interlocked.Add(ref sum, i);
}
Console.WriteLine($"Thread {(int)n} exiting resource pool");
// Exit resource pool
_pool.Release();
}
}
SemaphoreSlim does not require Close()
.
The code difference between the two is that simple.
Differences
If you instantiate Semaphore using the following constructor (parameter name cannot be empty), the created object is valid across the entire operating system.
public Semaphore (int initialCount, int maximumCount, string name);
SemaphoreSlim, on the other hand, is only effective within a process.
SemaphoreSlim does not enforce thread or task identity on calls to Wait
, WaitAsync
, and Release
.
In contrast, the Semaphore class strictly monitors this; if the corresponding call count does not match, an exception will occur.
It’s like having a pen holder with pens. Without monitoring, after using the pens, they should all be put back. If originally there were 10 pens, and if they are not put back after each use or if pens from other places are put in, then the final count will not be 10.
文章评论