Semaphore提供了更精细化的多线程控制,如果你看过上一节的Mutex介绍,那么你应该很容易理解Semaphore类。我们直接先以例子开头,然后在介绍这个类。
1.Semaphore实例
看下面的代码:
using System;
using System.Threading;
namespace SemaphoreDemo
{
class Program
{
public static Semaphore semaphore = null;
static void Main(string[] args)
{
try
{
//Try to Open the Semaphore if Exists, if not throw an exception
semaphore = Semaphore.OpenExisting("SemaphoreDemo");
}
catch(Exception Ex)
{
//If Semaphore not Exists, create a semaphore instance
//Here Maximum 2 external threads can access the code at the same time
semaphore = new Semaphore(2, 2, "SemaphoreDemo");
}
Console.WriteLine("External Thread Trying to Acquiring");
semaphore.WaitOne();
//This section can be access by maximum two external threads: Start
Console.WriteLine("External Thread Acquired");
Console.ReadKey();
//This section can be access by maximum two external threads: End
semaphore.Release();
}
}
}
现在你编译生成可执行文件后,连续点击三次,就可以看到如下情形:
如果你读到这儿,完全看懂了Semaphore的代码,我想你可能不需要继续往下读了。没错,Semaphore就是控制外部线程数量,从而可以让多个实例运行。
2.Semaphore类介绍
C#中的信号量类用于限制可以同时访问共享资源的外部线程的数量。换句话说,我们可以说Semaphore允许一个或多个外部线程进入临界区并在线程安全的情况下并发执行任务。因此,在实时情况下,当我们的资源数量有限并且我们想限制可以使用它的线程数量时,我们需要使用信号量。
2.1构造函数
C# 中的 Semaphore 类提供了以下四个构造函数,我们可以使用它们来创建 Semaphore 类的实例。
Semaphore(int initialCount, int maximumCount):它初始化 Semaphore 类的新实例,指定初始条目数和最大并发条目数。
Semaphore(int initialCount, int maximumCount, string name):它初始化 Semaphore 类的新实例,指定初始条目数和最大并发条目数,并可选地指定系统信号量对象的名称。
Semaphore(int initialCount, int maximumCount, string name, out bool createdNew):它初始化了一个新的Semaphore类实例,指定初始条目数和最大并发条目数,可选地指定系统信号量对象的名称,并指定一个变量,该变量接收一个值,该值指示是否创建了一个新的系统信号量。
Semaphore(int initialCount, int maximumCount, string name, out bool createdNew, SemaphoreSecurity semaphoreSecurity):它初始化一个Semaphore类的新实例,指定初始条目数和最大并发条目数,可选地指定系统名称信号量对象,指定一个变量,该变量接收一个值,该值表示是否创建了一个新的系统信号量,并指定了对系统信号量的安全访问控制。
信号量类构造函数中使用的参数:
initialCount:可以并发授予的信号量的初始请求数。如果 initialCount 大于 maximumCount,它会抛出 ArgumentException。
maximumCount:可以同时授予的信号量的最大请求数。如果 maximumCount 小于 1 或 initialCount 小于 0,它将抛出 ArgumentOutOfRangeException。
name:命名系统信号量对象的名称。
createdNew:当此方法返回时,如果创建了本地信号量(即,如果名称为 null 或空字符串)或创建了指定的命名系统信号量,则包含 true;如果指定的命名系统信号量已经存在,则为 false。此参数在未初始化的情况下传递。
semaphoreSecurity:一个 System.Security.AccessControl.SemaphoreSecurity 对象,表示要应用于指定系统信号量的访问控制安全性。
2.2成员函数
C#中的信号量类提供了以下方法:
OpenExisting(string name):此方法用于打开指定的已命名信号量(如果它已存在)。它返回一个代表命名系统信号量的对象。这里的参数name指定了要打开的系统信号量的名称。如果名称为空字符串,它将抛出 ArgumentException。- 或 - 名称超过 260 个字符。如果名称为空,它将抛出 ArgumentNullException。
OpenExisting(string name, SemaphoreRights rights):此方法用于打开指定的命名信号量(如果它已经存在)并具有所需的安全访问权限。它返回一个代表命名系统信号量的对象。这里的参数name指定了要打开的系统信号量的名称。参数权限指定代表所需安全访问的枚举值的按位组合。
TryOpenExisting(string name, out Semaphore result):该方法用于打开指定命名的Semaphore,如果已经存在,则返回一个值,表示操作是否成功。这里的参数name指定了要打开的系统Semaphore的名称。当此方法返回时,结果包含一个 Semaphore 对象,如果调用成功则表示命名的 Semaphore,如果调用失败则为 null。此参数被视为未初始化。如果命名的互斥体被成功打开,它返回真;否则,假的。
TryOpenExisting(string name, SemaphoreRights rights, out Semaphore result):此方法用于打开指定的命名信号量,如果它已经存在,具有所需的安全访问权限,并返回一个指示操作是否成功的值。这里的参数name指定了要打开的系统Semaphore的名称。参数权限指定代表所需安全访问的枚举值的按位组合。当此方法返回时,结果包含一个 Semaphore 对象,如果调用成功则表示命名的 Semaphore,如果调用失败则为 null。此参数被视为未初始化。如果命名的信号量打开成功,则返回真;否则,假的。
Release():此方法退出信号量并返回先前的计数。它在调用 Release 方法之前返回信号量的计数。
Release(int releaseCount):此方法退出信号量指定次数并返回之前的计数。这里,参数releaseCount指定退出信号量的次数。它在调用 Release 方法之前返回信号量的计数。
GetAccessControl():此方法获取指定系统信号量的访问控制安全性。
SetAccessControl(SemaphoreSecurity semaphoreSecurity):此方法设置命名系统信号量的访问控制安全性。
注意: C#中的Semaphore类继承自WaitHandle抽象类,WaitHandle类提供了我们需要调用的WaitOne()方法来锁定资源。请注意,信号量对象只能从获取它的同一个线程中释放。
2.3内部机制
信号量在 C# 中如何工作?
信号量是存储在操作系统资源中的 Int32 变量。当我们初始化信号量对象时,我们用一个数字初始化它。这个数字基本上用来限制可以进入临界区的线程。
因此,当线程进入临界区时,它会将 Int32 变量的值减 1,当线程退出临界区时,它会将 Int32 变量的值加 1。最重要的一点是您需要请记住,当 Int32 变量的值为 0 时,则没有线程可以进入临界区。
如何在 C# 中创建信号量?
您可以使用以下语句在 C# 中创建信号量实例。在这里,我们使用带有两个参数的构造函数的重载版本来创建信号量类的实例。
semaphoreObject = new Semaphore(initialCount: 2, maximumCount: 3);
正如在上面的语句中看到的,我们在初始化时将两个值传递给 Semaphore 类的构造函数。这两个值代表 InitialCount 和 MaximumCount。maximumCount 定义最多有多少个线程可以进入临界区,initialCount 设置 Int32 变量的值。
InitialCount 参数设置 Int32 变量的值。也就是说,它定义了可以并发授予的信号量的初始请求数。MaximumCount 参数定义可以同时授予的信号量请求的最大数量。
例如,如果我们将最大计数值设置为 3,初始计数值为 0,则表示已经有 3 个线程处于临界区,因此不再有新线程可以进入临界区。如果我们设置最大计数值为 3,初始计数值为 2。这意味着最多 3 个线程可以进入临界区,并且当前有 1 个线程处于临界区,因此可以有两个新线程进入临界区部分。
注1:线程进入临界区时,initialCount变量值减1,线程退出临界区时,initialCount变量值加1。当initialCount变量值为0时,则没有线程可以进入临界区。第二个参数 maximumCount 总是必须等于或大于第一个参数 initialCount 否则我们会得到一个异常。
注2:当线程要退出临界区时,我们需要调用Release()方法。调用此方法时,它会增加由信号量对象维护的 Int32 变量。
3.Semaphore举例
看一个稍微复杂点的例子:
using System;
using System.Threading;
namespace SemaphoreDemo
{
class Program
{
public static Semaphore semaphore = new Semaphore(2, 3);
static void Main(string[] args)
{
for (int i = 1; i <= 10; i++)
{
Thread threadObject = new Thread(DoSomeTask)
{
Name = "Thread " + i
};
threadObject.Start();
}
Console.ReadKey();
}
static void DoSomeTask()
{
Console.WriteLine(Thread.CurrentThread.Name + " Wants to Enter into Critical Section for processing");
try
{
//Blocks the current thread until the current WaitHandle receives a signal.
semaphore.WaitOne();
//Decrease the Initial Count Variable by 1
Console.WriteLine("Success: " + Thread.CurrentThread.Name + " is Doing its work");
Thread.Sleep(5000);
Console.WriteLine(Thread.CurrentThread.Name + "Exit.");
}
finally
{
//Release() method to release semaphore
//Increase the Initial Count Variable by 1
semaphore.Release();
}
}
}
}
运行结果如下:
具体过程就不分析了。
值得注意的是Semaphore与操作系统是有关联的,上面的例子可以跨平台运行,但是第一个例子因为使用OpenExisting函数,在linux上运行可能会失败。
4.SemaphoreSlim
如果的你的程序允许多个实例,互不影响,建议在每隔实例内部使用 C# 中的 SemaphoreSlim 类进行同步。轻量级信号量控制对应用程序本地资源池的访问。它代表了信号量的轻量级替代方案,它限制了可以并发访问资源或资源池的线程数。
4.1SemaphoreSlim类的构造函数和方法
C# 中 SemaphoreSlim 类的构造函数
C# 中的 SemaphoreSlim 类提供了以下两个构造函数,我们可以使用它们来创建 SemaphoreSlim 类的实例。
SemaphoreSlim(int initialCount):它初始化 SemaphoreSlim 类的新实例,指定可以并发授予的初始请求数。这里,参数initialCount指定了可以并发授予的信号量的初始请求数。如果 initialCount 小于 0,它将抛出 ArgumentOutOfRangeException。
SemaphoreSlim(int initialCount, int maxCount):它初始化 SemaphoreSlim 类的一个新实例,指定可以同时授予的初始和最大请求数。这里,参数initialCount指定了可以并发授予的信号量的初始请求数。而参数 maxCount 指定了可以并发授予的信号量的最大请求数。如果 initialCount 小于 0,或者 initialCount 大于 maxCount,或者 maxCount 等于或小于 0,它将抛出 ArgumentOutOfRangeException。
C#中SemaphoreSlim类的方法:
C# 中的 SemaphoreSlim 类提供了以下方法。
在 SemaphoreSlim 类中有多个可用的 Wait 方法的重载版本。它们如下:
Wait():它阻塞当前线程,直到它可以进入 System.Threading.SemaphoreSlim。
Wait(TimeSpan timeout):它阻塞当前线程,直到它可以进入SemaphoreSlim,使用一个TimeSpan来指定超时。当前线程成功进入SemaphoreSlim则返回true;否则,假的。
Wait(CancellationToken cancellationToken): 它阻塞当前线程,直到它可以进入 SemaphoreSlim,同时观察一个 CancellationToken。
Wait(TimeSpan timeout, CancellationToken cancellationToken):它阻塞当前线程,直到它可以进入SemaphoreSlim,使用指定超时的TimeSpan,同时观察一个CancellationToken。当前线程成功进入SemaphoreSlim则返回true;否则,假的。
Wait(int millisecondsTimeout):它阻塞当前线程,直到它可以进入 SemaphoreSlim,使用指定超时的 32 位带符号整数。当前线程成功进入SemaphoreSlim则返回true;否则,假的。
Wait(int millisecondsTimeout, CancellationToken cancellationToken):它会阻塞当前线程,直到它可以进入 SemaphoreSlim,使用指定超时的 32 位带符号整数,同时观察 CancellationToken。当前线程成功进入SemaphoreSlim则返回true;否则,假的。
参数:
以下是 Wait 方法中使用的参数说明。
timeout:表示等待毫秒数的 TimeSpan,表示 -1 毫秒无限期等待的 TimeSpan,或表示 0 毫秒的 TimeSpan 以测试等待句柄并立即返回。
cancellationToken:要观察的 System.Threading.CancellationToken。
millisecondsTimeout:等待的毫秒数,System.Threading.Timeout.Infinite(-1) 无限期等待,或零以测试等待句柄的状态并立即返回。
注意:上述所有方法的异步版本也可用
释放方式:
SemaphoreSlim 类中有两个 Release 方法的重载版本。它们如下:
Release():释放一次 SemaphoreSlim 对象。它返回 SemaphoreSlim 的先前计数。
Release(int releaseCount): 它释放 SemaphoreSlim 对象指定的次数。它返回 SemaphoreSlim 的先前计数。这里,参数releaseCount指定退出信号量的次数。
4.2SemaphoreSlim案例
这里直接贴一个官方的例子,比较典型了
using System;
using System.Threading;
using System.Threading.Tasks;
public class Example
{
// Create the semaphore.
private static SemaphoreSlim semaphore = new SemaphoreSlim(0, 3);
// A padding interval to make the output more orderly.
private static int padding;
public static void Main()
{
Console.WriteLine($"{semaphore.CurrentCount} tasks can enter the semaphore");
Task[] tasks = new Task[5];
// Create and start five numbered tasks.
for (int i = 0; i <= 4; i++)
{
tasks[i] = Task.Run(() =>
{
// Each task begins by requesting the semaphore.
Console.WriteLine($"Task {Task.CurrentId} begins and waits for the semaphore");
int semaphoreCount;
semaphore.Wait();
try
{
Interlocked.Add(ref padding, 100);
Console.WriteLine($"Task {Task.CurrentId} enters the semaphore");
// The task just sleeps for 1+ seconds.
Thread.Sleep(1000 + padding);
}
finally
{
semaphoreCount = semaphore.Release();
}
Console.WriteLine($"Task {Task.CurrentId} releases the semaphore; previous count: {semaphoreCount}");
});
}
// Wait for one second, to allow all the tasks to start and block.
Thread.Sleep(1000);
// Restore the semaphore count to its maximum value.
Console.Write("Main thread calls Release(3) --> ");
semaphore.Release(3);
Console.WriteLine($"{semaphore.CurrentCount} tasks can enter the semaphore");
// Main thread waits for the tasks to complete.
Task.WaitAll(tasks);
Console.WriteLine("Main thread Exits");
Console.ReadKey();
}
}
因为SemaphoreSlim支持异步等待,所以个人觉得一般情况下用SemaphoreSlim可能更好。运行结果如下:
0 tasks can enter the semaphore
Task 2 begins and waits for the semaphore
Task 1 begins and waits for the semaphore
Task 4 begins and waits for the semaphore
Task 5 begins and waits for the semaphore
Task 3 begins and waits for the semaphore
Main thread calls Release(3) --> 3 tasks can enter the semaphore
Task 4 enters the semaphore
Task 1 enters the semaphore
Task 2 enters the semaphore
Task 2 releases the semaphore; previous count: 1
Task 4 releases the semaphore; previous count: 0
Task 1 releases the semaphore; previous count: 2
Task 5 enters the semaphore
Task 3 enters the semaphore
Task 3 releases the semaphore; previous count: 1
Task 5 releases the semaphore; previous count: 2
Main thread Exits