线程同步 –Mutex和Semaphore

时间:2021-10-08 19:42:57

上一篇介绍了同步事件EventWaitHandle,以及它的两个子类型AutoResetEvent和ManualResetEvent。下面接着介绍WaitHandle的另外两个子类型Mutex和Semaphore。

互斥体Mutex

互斥体Mutex也是Windows用来进行线程同步的内核对象。当两个或更多线程需要同时访问一个共享资源时,可以使用 Mutex 同步基元,它只向一个线程授予对共享资源的独占访问权。 如果一个线程获取了互斥体,则要获取该互斥体的第二个线程将被挂起,直到第一个线程释放该互斥体。

说到这里,可以回想一下前面介绍的lock和Monitor,其实如果只是进行同一进程之间的线程同步,建议更多的使用lock和Monitor,因为Mutex示一个内核对象,所以使用的时候会产生很大的性能消耗;但是,Mutex可以支持不同进程之间的线程同步,同时允许同一个线程多次重复访问共享区,但是对于别的线程那就必须等待。因此,要根据应用的需求来选择lock/Monitor还是Mutex。

线程同步 –Mutex和Semaphore

对于Mutex,通过父类WaitHandle的WaitOne方法来请求互斥体的所属权。

下面看看Mutex中的常用方法:

  • 实例方法:
    • Mutex(bool initiallyOwned):创建Mutex实例,并设置互斥体的初始状态(终止/非终止)
    • Mutex(bool initiallyOwned, string name):通过name参数来创建一个命名的互斥体(也叫全局互斥体),通过这个名字可以进行不同进程之间的线程同步。反之,没有名称的互斥体被称为局部互斥体
    • Mutex(bool initiallyOwned, string name, out bool createdNew):如果两个进程会创建同名的Mutex,那么后执行的线程返回的Mutex实例只是指向了同名的Mutex而已。那么,这里我们可以通过createdNew参数来判断该进程中的Mutex实例是否是新创建的
    • ReleaseMutex():线程通过ReleasMutex方法释放互斥体的所属权
  • 静态方法:
    • OpenExisting(string name):打开指定名称为 mutex(如果已经存在)

互斥体Mutex也有终止和非终止状态,调用ReleaMutex 后互斥体的状态设定为终止,直到其他线程占有互斥体,但是如果没有线程拥有互斥体的话,该互斥体的状态将一直保持终止状态。

注意:WaitOne方法和ReleaseMutex方法的使用次数必须一致。前面介绍过互斥体Mutex允许同一个线程多次重复访问共享区,也就是说,拥有互斥体的线程可以在对 WaitOne 的重复调用中请求相同的互斥体而不会阻止其执行, 但线程必须调用 ReleaseMutex 方法同样多的次数以释放互斥体的所属权。

下面可以个互斥体Mutex的例子:

namespace MutexTest
{
class Program
{
private static Mutex mutex; static void Main(string[] args)
{
string mutexName = "MutexTest";
try
{
//尝试打开已有的互斥体
mutex = Mutex.OpenExisting(mutexName);
}
catch (WaitHandleCannotBeOpenedException e)
{
Console.WriteLine("Mutex named {0} is not exist, error message: {1}", mutexName, e.Message);
//Mutex实例初始为非终止状态
mutex = new Mutex(false, mutexName);
Console.WriteLine("Create Mutex {0}", mutexName);
} for (int i = ; i < ; i++)
{
//通过线程池调用时间打印函数
ThreadPool.QueueUserWorkItem(new WaitCallback(GetCurrentTime));
} Console.ReadLine();
}
static void GetCurrentTime(object state = null)
{
//调用WaitOne来等待信号
mutex.WaitOne();
try
{
//Mutex支持同一个线程多次重复访问共享区,所以加上下面的WaitOne一样可以工作
//mutex.WaitOne();
Thread.Sleep();
Console.WriteLine("Current Time is: {0}", DateTime.Now);
//mutex.ReleaseMutex();
}
finally
{
mutex.ReleaseMutex();
}
}
}
}

代码的输出为下:

线程同步 –Mutex和Semaphore

当我们连续两次运行MutexTest.exe时,可能得到以下的输出。通过输出可以看到,后面启动的进程可以获得同样的互斥体,然后进行进程之间的线程同步。

线程同步 –Mutex和Semaphore

通过Mutex运行单一程序

由于命名的Mutex可以跨越进程,所以Mutex还有一个常用的场景就是用来限制一次只能运行一个程序实例。直接看一个简单的例子:

namespace SingleAppWithMutex
{
class Program
{
static void Main(string[] args)
{
bool canCreateNew; //通过canCreateNew来判断是否是新建的互斥锁实例
Mutex m = new Mutex(true, "SingleApp", out canCreateNew);
if (canCreateNew)
{
Console.WriteLine("Start App to print time");
new Thread(() => {
while (true)
{
Thread.Sleep();
Console.WriteLine("Current time is {0}", DateTime.Now);
}
}).Start();
}
else
{
Console.WriteLine("App already start");
} Console.Read();
}
}
}

在程序运行过程中,如果尝试再次启动程序,就会得到"App already start "输出。这里主要利用了Mutex(bool initiallyOwned, string name, out bool createdNew)构造函数中的createdNew这个参数。

信号量Semaphore

前面所介绍的线程同步机制中,都是同时只能有一个线程进入共享区(ManualResetEvent除外),这里我们看一下通过信号量进行线程同步,这种线程同步方式也可以支持多个线程同时进入共享区。

信号量通过一种计数的方式来控制同时进入共享区的线程的数量,信号量的计数在每次线程进入信号量时减小,在线程释放信号量时增加。 当计数为零时,后面的请求将被阻塞,直到有其他线程释放信号量。 当所有的线程都已释放信号量时,计数达到创建信号量时所指定的最大值。

线程同步 –Mutex和Semaphore

对于信号量,也是通过调用WaitOne 方法(从 WaitHandle 类继承)进入信号量。

下面看看Semaphore中的常用方法:

  • 实例方法:
    • Semaphore(int initialCount, int maximumCount):创建信号量实例,并设置初始计数值和最大计数值
    • Semaphore(int initialCount, int maximumCount, string name):创建命名的信号量,可以支持进程之间的线程同步
    • Release():退出信号量,并且计数加一
    • Release(int releaseCount):以指定的次数退出信号量,并更新计数

信号量分为两种类型:局部信号量和已命名的系统信号量。已命名的系统信号量在整个操作系统中都可见,可用于同步进程活动。 您可以创建多个 Semaphore 对象来表示同一个已命名的系统信号量,也可以使用 OpenExisting 方法打开现有的已命名系统信号量。

注意:Semaphore 类不对 WaitOne 或 Release 调用强制线程标识。 程序员负责确保线程释放信号量的次数不能太多。 例如,假定信号量的最大计数为二,线程 A 和线程 B 都进入信号量。 如果线程 B 中的编程错误导致它两次调用 Release,则两次调用都成功。 这样,信号量的计数就已经达到了最大值,所以,当线程 A 最终调用 Release 时,将引发 SemaphoreFullException

下面看一个信号量的例子:

namespace SemaphoreTest
{
class Program
{
//创建信号量实例,
private static Semaphore sem; static void Main(string[] args)
{
bool createdNew;
sem = new Semaphore(, , "SemaphoreTest", out createdNew);
if (createdNew)
{
Thread.Sleep();
//退出信号量三次,并且计数加三
sem.Release();
} for (int i = ; i < ; i++)
{
Thread t = new Thread(new ParameterizedThreadStart(Worker));
t.Start(i);
} Console.Read();
} private static void Worker(object index)
{
//调用WaitOne来等待信号
sem.WaitOne();
Console.WriteLine("---> Thread {0} enter Critical code section", index.ToString());
Random ran = new Random();
Thread.Sleep(ran.Next(, ));
Console.WriteLine(" Thread {0} exit Critical code section <---", index.ToString());
//退出信号量,并且计数加一
sem.Release(); }
}
}

代码的输出为,由于代码中信号量实例的最大计数为三,所以可以同时访问共享去的最大线程数就是三。

线程同步 –Mutex和Semaphore

当我们连续执行SemaphoreTest.exe两次时,可以看到两个进程之间的线程同步,依然同时只能有最多三个线程进入共享区。

总结

本文介绍了如何通过互斥体Mutex和信号量Semaphore进行线程同步。

到这里,同步句柄WaitHandle 及其子类EventWaitHandle,AutoResetEvent,ManualResetEvent,Mutex和Semaphore就都介绍完了。通过内核对象进行线程同步会带来很大的性能开销,但是,由于内核对象属于操作系统,对所有进程可见,所以利用这些线程同步方式可以很容易的实现不同进程之间的线程同步。