关于c#多线程中的几个信号量

先言

信号量在c#多线程通信中主要用来向阻塞的线程传达信号从而使得阻塞线程继续执行

多线程信号(线程交互):通常是指线程必须等待一个线程或者多个线程通知交互(释放信号)才可以继续执行
在c#中信号量主要有这几个 AutoResetEvent,ManualResetEvent,CountdownEvent,EventWaitHandle,Semaphore

信号量

信号量状态,有信号状态即不会阻塞线程、无信号状态会去阻塞线程。wainOne方法会阻塞线程,当waitOne方法具有阻塞时间量的时候则无需等待信号量的释放,等时间到了会去自动执行waitOne方法后的语句

AutoResetEvent

AutoResetEvent 在释放信号量后,会默认设置为无信号状态。AutoResetEvent 构造函数会传递一个initialState boolean 类型的参数,参数为false 时 需要主动去传递信号量,传递信号量之后将重新设置为无信号状态。参数为ture 时会自动设置为有信号状态,大体意思就是,会默认执行阻塞线程,不需要阻塞线程收到信号量才会执 行(不会阻塞调用线程)。在参数为ture 时,AutoResetEvent 类实例调用 Reset () 方法后,会将当前AutoResetEvent 类实例设置为无信号状态也就是 变成了一个 参数为 false 的 AutoResetEvent 类实例,在此之后的执行阻塞线程都需要主动去释放(传递)信号。

private static AutoResetEvent auto = new AutoResetEvent(false);
private static AutoResetEvent auto = new AutoResetEvent(ture);//有信号终止状态
Thread thread1 = new Thread(AutoResetEventHandler);
            Console.WriteLine("当前线程id"+Thread.CurrentThread.ManagedThreadId);
            thread1.Start();
            Thread.Sleep(5000);
            auto.Set();
           // auto.Reset();  在这种情况下new AutoResetEvent(ture) 的 类实例 会变成无信号未终止状态的 如果阻塞线程没有接收到信号量将会一直阻塞下去,直到接收到信号量
            Thread thread2 = new Thread(AutoResetEventHandlerTwo);
            thread2.Start();
            Thread.Sleep(3000);//等待3秒
private static void AutoResetEventHandler()
        {
            Console.WriteLine("当前线程id" + Thread.CurrentThread.ManagedThreadId);
            auto.WaitOne();//阻塞线程
            Console.WriteLine("等待一秒后执行");
        }
private static void AutoResetEventHandlerTwo()
        {
            auto.WaitOne();//阻塞线程
            Console.WriteLine("我是第二个等待执行");
        }

ManualResetEvent

ManualResetEvent 与上面的AutoResetEvent 类似在构造函数中也会传入一个Boolean类型参数,不同的是信号量的释放,AutoResetEvent在信号量释放后会自动设置为无信号状态(未终止状态),ManualResetEvent 需要我们手动调用Reset()方法将其设置为无信号量状态(未终止状态),否则其会一直保持有信号量状态(终止状态)ManualResetEvent 如果不手动重置信号量状态,阻塞线程将不会起作用,会立即执行。这里有个注意点,EventReseMode.ManualReset等待句柄上有一个或多个等待线程,我们要注意Rese()的时机,等待线程恢复执行前是需要一定的执行时间的,我们无法判断那个等待线程恢复到执行前,在调用Reset()方法可能会中断等待线程的执行。如果我们希望在所有的等待线程都执行完后开启新的线程,就必须将他阻止到等待线程都完成后去发送新的信号量执行新的任务。AutoResetEvent 不会存在这个情况,因为它会自动重置为无信号状态。这里我们可以看看官方的说法

private static ManualResetEvent manualReset = new ManualResetEvent(false);
private static ManualResetEvent manualReset = new ManualResetEvent(true);
Thread thread1 = new Thread(() => {
                manualReset.WaitOne();
                Console.WriteLine("最开始的执行");
            });
            thread1.Start();
            Thread.Sleep(3000);//休眠--等待三秒
            manualReset.Set();//释放信号量
            Thread thread2 = new Thread(ManualResetEventHandler1);
            thread2.Start();
            manualReset.Reset();//重置信号量
            Thread thread3=new Thread(ManualResetEventHandler2);
            manualReset.Set();//释放信号量
            thread3.Start();
            manualReset.Reset();
private static void ManualResetEventHandler1()
        {
            manualReset.WaitOne();
            Console.WriteLine("第一次等待执行");
        }
        private static void ManualResetEventHandler2()
        {
            manualReset.WaitOne();
            Console.WriteLine("第二次等待执行");
        }

上面说到过,ManualResetEvent 构造函数与AutoResetEvent构造函数是一样,通过bool类型的参数判读 类的实例是否默认释放信号量,不同的是ManualResetEvent 需要手动调用Reset()方法。上面代码中,我们传递了一个false参数,调用了Set()方法释放信号量,然后再调用Reset()方法重置信号量,如此反复一次,ManualResetEventHandler2 会一直阻塞 直到我们释放信号量,才会继续执行。

CountdownEvent

CountdownEvent 实例化是需要传入一个int 类型作为InitialCount初始值,CountdownEvent信号量的释放很特别,只有当Countdown类的实例的CurrentCount等于0时才会释放我们的信号量,Signal()方法每次调用都会使得CurrentCount进行-1操作。Reset()方法会重置为实例化对象时传递的参数值,也可以Reset(100)对我们的InitialCount重新赋值。

private static CountdownEvent countdownEvent = new CountdownEvent(1000);
 CountReduce();
           // countdownThread.Start();
            Thread thread = new Thread(() => {
                countdownEvent.Wait();
                Console.WriteLine("直到CountdownEvent总数="+countdownEvent.CurrentCount+"我才执行");
                //CountdownEvent.CurrentCount//当前总数
                //CountdownEvent.AddCount()//添加1
                //CountdownEvent.AddCount(10);//添加指定数量
                //CountdownEvent.InitialCount//总数
                //CountdownEvent.Reset()//设置为InitialCount初始值
                //CountdownEvent.Reset(100)//设置为指定初始值
            });
            thread.Start();
private static async Task CountReduce()
        {
           await Task.Run(async () => {
               for(var i = 0; i < 1000; i++)
                {
                    await Task.Delay(100);//休眠100毫秒--等到100毫秒
                   //if (countdownEvent.CurrentCount < 10)
                   //{
                   //    countdownEvent.Reset(100);
                   //    CountReduce();
                   //}
                   countdownEvent.Signal();
                    Console.WriteLine("当前总数"+countdownEvent.CurrentCount);
                }
            });
        }

上面代码中我们有用到异步方法但没有等待结果,但是但是线程的委托方法中调用了 countdownEvent.Wait()来阻塞线程;,只有当我们的CurrentCount等于0时才会释放信号量线程才不会阻塞得以继续执行(有感兴趣的可以试试这部分的代码)

EventWaitHandle

本地事件等待句柄是指创建EventWaitHandle 对象时指定EventResetMode枚举,可分为自动重置的事件等待句柄和手动重置的事件等待句柄。
关于事件等待句柄,不涉及到.NET 事件以及委托和事件处理程序,我们可以看一下官方的声明。

EventWaitHandle 微软的解释是本地事件等待句柄,但这和c#中的事件是毫无关系的,这里指的是操作系统的事件。EventWaitHandle 实例化需要两个参数Bool、EventResetMode 。布尔类型的参数是指是否具有信号量,EventMode 是个枚举,枚举值有AutoResetEvent,ManulResetEvent,到这里的用法就跟AutoResetEvent、MnaulResetEvent 差不多了。

SignalAndWait

EventWaitHandle 是继承WaitHandle的,WaitHandler又实现了许多静态,SignalAndWait 就是其中一个。

SignalAndWait 需要传递的两个参数都是WaitHandle 的实例,第一个参数是指需要传递信号量的watihandle 实例,也就是说无论我这个handler 之前是有状态的还是无状态的,他都会去执行,第二个参数是指需要中断的handler

EventWaitHandle manulEventWaitHandler = new EventWaitHandle(false,EventResetMode.ManualReset);
EventWaitHandle autoEventWaitHandler = new EventWaitHandle(false,EventResetMode.AutoReset);
Thread autothread = new Thread(EventWaitHandlerAuto);
Thread manulThear = new Thread(EventWaitHandlerManul);
autothread.Start();
manulThear.Start();
EventWaitHandle.SignalAndWait(manulEventWaitHandler, autoEventWaitHandler);
autoEventWaitHandler.Set();//在这里将信号量变成有状态的,保证EventWaitHandlerManul的执行
void EventWaitHandlerAuto()
{
    Console.WriteLine("waithandleAuto 开始");
    autoEventWaitHandler.WaitOne();
    Thread.Sleep(3000);
    Console.WriteLine("auto---休眠四秒结束");
}
void EventWaitHandlerManul()
{
    Console.WriteLine("waithandleManul 开始");
    manulEventWaitHandler.WaitOne();
    Thread.Sleep(3000);
    autoEventWaitHandler.Set();//这里将信号量变成有状态是没有用的,因为在SignalAndWait 方法中它是被中断的,这个时候可以在外部去将信号量变成有状态的
    Console.WriteLine("manul--休眠三秒结束");
}

Semaphore

Semaphore 可以限制同时进入的线程数量。Semaphore 的构造函数有两个int 类型的参数,第一是指允许同时进入线程的个数,第二个是指最多与同时进入线程的个数,并且第二个参数时不能小于第一个参数(毕竟同时进入的不能大于最大能容纳下的)。WaitOne()方法这里的与上面几个信号量有点小小的不同,每调用一次Semaphore释放的信号灯数量减一,当信号灯数量为0时会阻塞线程,Release()方法会对我们的信号灯数量进行加一操作(释放信号灯),也可以调用Release(int i)来指定释放的信号灯数量。这里有个注意点,我们可以在程序中多次调用Release方法(),但要保证在程序中释放的信号量不能大于最大信号量。

private static Semaphore semaphore = new Semaphore(2, 5);//本地信号灯
for (var i = 0; i < 12; i++)
            {
                Thread thread = new Thread(new ParameterizedThreadStart(Semaphorehandle));
                thread.Start(i);
            }
 private static void Semaphorehandle(Object i)
        {
            semaphore.WaitOne();
            Console.WriteLine((int)i + "进入了线程");
            Thread.Sleep(2000);
            Console.WriteLine((int)i + "准备离开线程");
            if ((int)i >1)
            {
                Console.WriteLine(semaphore.Release());
                return;
            }
            semaphore.Release(2);
        }

这里插一句——多线程执行是没有特定的顺序的、是不可预测的。
Semaphore信号灯有两种:本地信号灯和命名系统信号灯。本地信号灯仅存在于进程中(上面的例子中使用的是本地信号灯)。命名系统信号灯是存在与整个操作系统的,一般用于同步进程的活动。
c#多线程的信号量就先到这了。
也可以看大佬的教程
本文涉及到的Demo代码

写在最后

写到这里,应该是距写这篇文章差不多也有10个月左右了,因为最近在找工作的原因,又来回顾这篇文章了,结果发现了有许多看不懂的地方和错误,目前也已改正,也许还存在部分错误我无法发现,若发现错误还望各位告知,并且也告诉自己做技术写文章需要严谨的态度的。

posted @ 2022-05-29 23:33  布吉岛1c  阅读(1406)  评论(3编辑  收藏  举报