多线程合集(一)---信号量,锁,以及并发编程,自定义任务调度和awaiter
引言
在后端开发中,多线程技术总是后端开发中常用到的技术,那什么是多线程呢,在操作系统中,程序运行的最小单位是进程,那线程则是进程里面的最小单位,关系是一对多的关系,而线程的调度,是由操作系统的时间片算法进行调度的,即在某一个时间段内只有一个线程去进行计算,其他的则在等待,这涉及的系统方面的知识,我也是一知半解,本文主要是讲解c#中多线程的常用操作,以及根据微软提供的抽象类和接口去实现自定义的一些拓展,多线程方面会有至少两篇文章,第一篇也就是本文,着重讲解代码片段,后面会讲解async和await的原理,以及运行时自定义状态机的IL代码转为c#代码,并且讲解 他的执行顺序。如有疑问,敬请提出,大家一起学习。
信号量
在c#中信号量,可以用线程之间的通讯,主要用来某一线程阻塞,然后在由另一线程去进行发出信号,让阻塞的线程有信号量,从而继续执行,其中c#信号量主要分为AutoResetEvent,ManualResetEvent,CountdownEvent,EventWaitHandle,Semaphore。其中第一个和第二个有些类似,第一个是在线程收到信号然后释放后,自动的设置为无信号状态,等待下一次的释放,第二个是需要手动reset这是这两个的区别,这两个的构造函数中有个bool参数,意思是,true情况下是终止状态,即可以理解为设置为true的情况下默认是有信号的,那么下方Wait调用中不会阻塞会直接执行,False的情况下是默认没有信号,需要代码中Set释放信号,即遇到Wait代码段会阻塞,等待其他线程进行set释放信号,第三个则是一个反向计数的一个信号量,具体是在创建对象的时候设置一个初始值,然后执行期间执行到Wait方法线程会阻塞,等待这个对象调用Signal方法的时候计数器会-1,妹调用一次就-1,直到归0时,阻塞线程继续执行,这个是很有意思的一个信号量,这也包含一些方法即AddCount方法可以每次添加一个 可以添加固定的数量,也可以reset初始值,也可以reset到自定义的一个值为初始值。第四个EventWaitHandle实际上是一个结合第一个和第二个的一个信号量,在创建对象的时候可以指定是手动还是自动,第一个bool参数和第一个第二个的bool参数意义一样,第三个和第四个参数是这个信号量的名称,以及是否重新创建的,如果参数out值是true说明是重新创建,否则是存在的信号量。最后一个是限制同时进入线程数量,构造函数的第一个参数是可以授予信号的初始数量,第二个参数为可以授予信号量的最大数量,即初始的时候可以有多少个被授予可以进入线程资源的数量,第二个是并发情况下最大可以有多少个线程去获取到信号量,第一个和第二个可以一样可以不一样,但是第一个不能小于第二个。
其实在c#信号量中,以及部分c#锁都是基于一个基类去实现的就是WaitHandle,这个类是一个抽象类,提供了一些静态方法,所以信号量和锁中很多都是基于这个实现的,在这个类中,包括了等待的方法,可以等待所有,可以等待某一个或者,一批,还有一个比较有意思的方法SignalAndWait是给某个信号量发送信号,让阻塞的子线程继续执行,然后将某个信号量Wait中断阻塞,言简意赅就是,这个方法有两个参数,第一个参数的意思就是需要发送信号的信号量,第二个参数是需要中断等待的信号量。接下来,让我们在代码中去实际看一下这些个信号量。
AutoResetEvent
private static AutoResetEvent auto = new AutoResetEvent(false);
Thread thread = new Thread(AutoReset);//定义线程去执行AutoReset方法 thread.Start();//开始线程 Thread.Sleep(5000);//休眠5s auto.Set();
static void AutoReset() { auto.WaitOne();//阻塞线程,等待释放信号从而继续执行下面的代码,一直等待 //auto.WaitOne(2000);//等待两秒,如果没有收到信号,则继续执行 Console.WriteLine("Wait 5s,Begin Run AutoReset"); }
在上述代码中,我们定义了一个AutoResetEvent的信号量,并且将它设置为无信号未终止状态,然后我们启动线程去执行AutoReset方法,在执行这个方法的时候到了执行的时候,我们调用WaitOne方法将子线程阻塞,等待主线程释放信号,在继续执行,然后输出我们想要的信息,那实际的意思是我们主线程等待5s之后去释放信号,在主线程Set方法调用之后,子线程的WaitOne方法在收到信号之后会去继续执行下面的代码,输出信息。
ManualResetEvent
private static ManualResetEvent manualResetEvent = new ManualResetEvent(false);
{ Thread thread = new Thread(ManualResetEvent);//定义线程去执行AutoReset方法 thread.Start();//开始线程 Thread.Sleep(5000);//休眠5s manualResetEvent.Set(); Thread.Sleep(5000); manualResetEvent.Reset();//和AutoResetEvent区别在于 AutoResetEvent会自动重置状态,ManualResetEvent需要手动Reset为无信号状态,否则二次或者多次waitone无效 }
static void ManualResetEvent() { manualResetEvent.WaitOne();//阻塞线程,等待释放信号从而继续执行下面的代码,一直等待 //auto.WaitOne(2000);//等待两秒,如果没有收到信号,则继续执行 Console.WriteLine("Wait 5s,Begin Run manualResetEvent"); }
我们定义了一个ManualResetEvent变量,设置为无信号终止状态,然后启动线程去执行ManualResetEvent方法,这个方法进入之后会阻塞子线程,等待收到信号继续执行,在主线程休眠5s之后释放信号,子线程收到信号继续执行,同AutoResetEvent一样的使用,只是最后我们加了一个Reset方法,会重新将这个设置为无信号状态,这样如果二次调用WaitOne的时候 还是需要等待子线程进行Set否则不会等待,直接执行。Reset实际上就是如果我们多次调用了WaitOne方法,那第一个线程执行后,如果不Reset,那么第二个或者后面的WaitOne都会立即执行不会等待,因为Reset是将信号重新设置为无信号状态。
CountdownEvent
var CountdownEvent = new CountdownEvent(1000); //CountdownEvent.CurrentCount//当前总数 //CountdownEvent.AddCount()//添加1 //CountdownEvent.AddCount(10);//添加指定数量 //CountdownEvent.InitialCount//总数 //CountdownEvent.Reset()//设置为InitialCount初始值 //CountdownEvent.Reset(100)//设置为指定初始值 Task.Run(() => { for (int i = 0; i < 1000; i++) { Task.Delay(100); CountdownEvent.Signal();//代表计数器-1 Console.WriteLine(CountdownEvent.CurrentCount); } }); CountdownEvent.Wait();//等待计数器归0 Console.WriteLine("结束");
我们定义了一个CountdownEvent变量,将初始值设置为1000,然后我们启动线程去循环1000,然后让计数器逐渐-1,同时主线程会调用Wait方法,这个方法会等待子线程Signal方法逐渐递减为0的似乎继续执行,即每次调用Signal方法,CurrentCount都会-1直到为0,主线程继续执行输出结束。
EventWaitHandle
#region 同AutoResetEvent Thread thread = new Thread(EventWaitHandleAutoReset);//定义线程去执行AutoReset方法 thread.Start();//开始线程 Thread.Sleep(5000);//休眠5s eventWaitHandle.Set();//如果下方调用SignalAndWait则可以此处注释掉 #endregion #region ManualResetEvent thread = new Thread(EventWaitHandleManualReset);//定义线程去执行AutoReset方法 thread.Start();//开始线程 //Thread.Sleep(5000);//休眠5s //eventWaitHandleManualReset.Set();同ManualReset一样 下方方法之所以Set 因为下面发了一个信号,并且等待了一个线程, WaitHandle.SignalAndWait(eventWaitHandle, eventWaitHandleManualReset);//eventWaitHandle发出信号Set,eventWaitHandleManualReset阻塞线程等待信号,EventWaitHandleManualReset发出信号后可以执行Console。WriteLine,否则一直阻塞 Console.WriteLine(); #endregion
static void EventWaitHandleAutoReset() { eventWaitHandle.WaitOne();//阻塞线程,等待释放信号从而继续执行下面的代码,一直等待 //auto.WaitOne(2000);//等待两秒,如果没有收到信号,则继续执行 Console.WriteLine("Wait 5s,Begin Run EventWaitHandle AutoReset"); Thread.Sleep(5000); } static void EventWaitHandleManualReset() { Thread.Sleep(5000);//休眠5s等待SignalAndWait阻塞线程,此处释放 eventWaitHandleManualReset.Set(); //eventWaitHandleManualReset.WaitOne();//阻塞线程,等待释放信号从而继续执行下面的代码,一直等待 //auto.WaitOne(2000);//等待两秒,如果没有收到信号,则继续执行 Console.WriteLine("Wait 5s,Begin Run EventWaitHandle ManualReset"); }
这里实际上我们不做过多的讲解,因为大多数都是和AutoResetEvent还有ManuallResetEvent一样,着重说一下SignalAndWait方法,可以看到在最开始的时候我们调用了EventWaitHandleAutoReset方法,但是我们的主线程是没有释放信号的,那他一直在哪里中断阻塞,在最后的代码中,我们又去EventWaitHandleManualReset调用了这个方法,在这个方法中我们用eventWaitHandleManualReset发出了信号调用了Set方法,在第一段的代码最后我们调用可SignalAndWait方法,传入了EventWaitHandle以eventWaitHandleManualReset
在这个方法中,会将第一个信号量释放信号,从而EventWaitHandleAutoReset方法收到信号后继续执行,那然后我们又阻塞了主线程,在子线程eventWaitHandleManualReset方法中我们又调用了Set方法释放信号,从而主线程继续执行,那如果我们在这里不调用Set方法那实际上主线程将会一直阻塞,当然这个方法中还有其他参数设置超时,以及是否退出上下文,这里不做过多的讲解,还需要各位去自己手动体验一下。
Semaphore
private static Semaphore Semaphore = new Semaphore(3, 3);
for (int i = 0; i <= 10; i++) { Thread thread = new Thread(new ParameterizedThreadStart(SemaphoreTest)); thread.Start(i); }
Semaphore.WaitOne();//阻塞线程,等待计数器小于设置的初始值后可以进入 Console.WriteLine(state + "进入了资源"); Thread.Sleep((int)state * 1000); Semaphore.Release();//释放信号,计数器+1 Console.WriteLine(state + "离开了了资源"
接下来是信号量中的最后一个,Semaphore,可以看到,主线程中我们是启动了十个线程去进行执行方法,但是我们定义中只设置了刚开始只能有三个进入并且在最大只有三个,可以在结果的控制台输出中看到,我们最后的结果输出图中,每次可以进入这个方法中执行的只会有三个线程,同时最大也只有三个线程,3,2,5进入之后,2然后离开,0进入,那里面是0,3,5,然后0离开之后1进入了,里面就是1,3,5,然后3离开之后4进入了,里面就是1,4,5,然后1离开之后6进入了,里面就是4,5,6,然后5离开7进入,就是4,6,7,然后4离开8进入里面就是6,7,8,然后6离开9进入了,里面就是7,8,9,然后7离开之后10进入里面就是8,9,10,然后8,9,10离开,可以看到信号量子线程中最多是只有三个线程可以获取到资源,其他线程需要等待进入的线程释放信号量然后在进入,就是调用了Release方法让计数器+1;这里面的输出可能有的同学会觉得不准确是因为离开资源的输出是在释放之后进行的,所以会出现这种情况。具体使用还是希望大家能够自己模拟,结合具体场景使用。
多线程锁
在c#中,多线程方面的锁分为Monitor,以及Mutex,读写锁ReaderWriterLockSlim以及自旋锁SpinLock,其中Lock关键字是根据Monitor里面的两个方法进行封装的,是Enter方法和Exit方法,Mutex是一个可以跨进程的一个同步基元,读写锁是一写多读,即一瞬间只允许一个线程去进行写操作,多个线程去进行读,当然还包括了读锁升级为写锁,自旋锁则是使用方式是和Lock可以说很像,但Lock是线程阻塞的情况下去让占用线程去执行代码段的,而自旋锁是加入有线程已经获取到了锁,那其他线程需要获取锁不是像Lock那样去进行阻塞等待,而是在重复的循环中去获取锁,直到获取到了锁,Lock简单就是阻塞等待,SpinLock是循环等待,SpinLock不适用阻塞时间长的业务场景,因为过多的旋转会出现性能方面的问题,同时也不建议是同意上下文中存在多个自旋锁,具体的优缺点可以着重看一下官方文档,传送门:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.spinlock?view=net-6.0。
Monitor
var monti = new MonitorTest(5, 10); foreach (var item in Enumerable.Range(0, 10)) { Task.Run(() => { Monitor.Enter(monti);//lock关键字是根据Enter 和Exit进行封装的 monti.Add(); Monitor.Exit(monti); }); } foreach (var item in Enumerable.Range(0, 10)) { Thread thread = new Thread(() => { var IsGetLock = false; Monitor.TryEnter(monti, ref IsGetLock);//在指定的毫秒内去获取排他锁,超过则不获取,ref 参数代表是否获取到排他锁 while (!IsGetLock) { Monitor.TryEnter(monti, ref IsGetLock);//在指定的毫秒内去获取排他锁,超过则不获取,ref 参数代表是否获取到排他锁 } Console.WriteLine(Thread.CurrentThread.Name + "获取"); monti.Add();//此方法内部sleep 9000 故是可以获取到 Monitor.Exit(monti);//让出锁 }); thread.Name = item.ToString(); thread.Start(); } //TryEnter另一种写法判断是否获取到锁 foreach (var item in Enumerable.Range(0, 10)) { Thread thread = new Thread(() => { if (Monitor.TryEnter(monti))//在指定的毫秒内去获取排他锁,超过则不获取,ref 参数代表是否获取到排他锁 { Console.WriteLine(Thread.CurrentThread.Name + "获取"); monti.Add();//此方法内部sleep 9000 故是可以获取到 Monitor.Exit(monti);//让出锁 } else { while (!Monitor.TryEnter(monti)) { Monitor.TryEnter(monti);//在指定的毫秒内去获取排他锁,超过则不获取,ref 参数代表是否获取到排他锁 } Console.WriteLine(Thread.CurrentThread.Name + "获取"); monti.Add();//此方法内部sleep 9000 故是可以获取到 Monitor.Exit(monti);//让出锁 } }); thread.Name = item.ToString(); thread.Start(); }
public class MonitorTest { public MonitorTest(int i, int j) { I = i; J = j; } private int I { get; set; } private int J { get; set; } public int Add() { Thread.Sleep(9000); Console.WriteLine(Thread.CurrentThread.Name + " Add"); I = I + J; J = I * J; return I + J; } public int Sub() { Thread.Sleep(5000); Monitor.Pulse(this); Console.WriteLine(Thread.CurrentThread.Name + "Pulse 锁"); return 0; } public void WaitTest() { Console.WriteLine(Thread.CurrentThread.Name + "进入了WaitTest"); Monitor.Wait(this); Console.WriteLine(Thread.CurrentThread.Name + "结束了WaitTest"); } public void PulseTest() { Console.WriteLine(Thread.CurrentThread.Name + "进入了PulseTest"); Monitor.Pulse(this); Console.WriteLine(Thread.CurrentThread.Name + "结束了PulseTest"); } }
众所周知,Lock关键字是根据Monitor静态类去进行实现的,Enter方法和Exit方法,在Enter的时候会锁住对象,由一个线程持有,这个对象的控制权,在Exit方法在释放对象的控制权,那实际上Monitor还提供了多种获取锁的方式,尝试获取锁如果获取到了则去进行下一步的逻辑,TryEnter方法是用来判断是否获取到了锁,里面的参数以及返回值都可以判断是否获取到了锁。实际上Enter和Exit只是Moniter的比较常用的俩个方法,实际上还有两个比较有意思的方法Wait和Pulse方法,这两个方法同样也是需要传入等待的对象或者释放的对象,第一个方法是将当前线程阻塞起来,放到等待队列中去,第二个方法是将当前对象的持有线程并且在等待队列中的放到就绪队列中等待继续执行,但是调用这个方法之后不是立即去执行接下来的操作的,因为是按照就绪队列中第一个的位置去进行执行的,释放掉的会在最后一个等待着就绪队列的顺序执行。
foreach (var item in Enumerable.Range(0, 10)) { Thread thread = new Thread(() => { Monitor.Enter(monti); if (item < 5) { monti.WaitTest();//这边将线程放入等待队列 } else { monti.PulseTest(); // 这里将线程放入就绪队列,然后继续执行, } Monitor.Exit(monti);//让出锁0,5 1,6 2,7, 3,8 4,9 }); thread.Name = item.ToString(); thread.Start(); }
我们可以看到在上面的方法中我们调用了WaitTest和PulseTest的两个方法,并且开启了是个线程,其中小于5的即0,1,2,3,4,这四个线程会被放入等待队列,等待释放继续执行,在5,6,7,8,9,我们又把等待队列中的释放移到就绪队列中让子线程继续执行,接下来我们看看输出的结果。
可以看到0,1,2,3,4线程阻塞都没有输出结束了WaitTest,那接下来PulseTest方法执行后,5释放了0,从而0继续执行输出了结束了WaitTest,然后6进入PulseTest方法释放了1,从而输出结果,然后是7和2,8和3,9和4。Wait和Pulse方法还是比较有意思的方法,虽然我们平常中基本上很少用到,但是我觉得至少有个知识储备,我觉得从程序员就应该有追根朔源的能力,并且就是我可以不用,但必须得会哈哈哈,这是我的一个想法。言归正传,这两个方法,可以具体的结合自身场景去使用。
Mutex
Mutex是一个可以跨进程的一个同步基元,构造函数有最多有三个参数,第一个参数表示当前线程是否具有Mutex初始所有权,第二个为同步基元的名称,第三个参数为Out参数,代表是否是新建的,false为系统已经存在同名的同步基元,true为是新建的,false情况下可以使用OpenExisting方法来获取同名的同步基元,这个方法是一个静态方法,当然还有一个TryOpenExisting来获取。这个锁中主要用的两个方法控制线程访问的有WaitOne方法以及ReleaseMutex方法来释放控制权。
foreach (var item in Enumerable.Range(0, 10)) { Thread thread = new Thread(() => { Console.WriteLine(Thread.CurrentThread.Name + "执行了线程"); mutex.WaitOne();//获取锁的所有权即等待对象被释放, Console.WriteLine(Thread.CurrentThread.Name + "获得了互斥锁"); string ss = ""; mutex.ReleaseMutex(); //释放锁的所有权,释放对代码块对象的所有权 Console.WriteLine(Thread.CurrentThread.Name + "释放了互斥锁"); }); thread.Name = item.ToString(); thread.Start(); } Thread.Sleep(10000);
在上面的代码中,我们可以看到开启十个线程,在调用了Wait方法后,获取锁然后执行下面的代码,未获取的会继续等待,在下方调用了Release方法释放锁,等待下一个线程进入。
从我们的结果可以看到,获取的只会有一个获取到,其他需要等到释放之后才能继续获取。这个类实际上还有很多功能,待你们一一探索,跨进程这里不做代码解释,之前又在项目中用到过。同时还有其他的用处,这里需要看官去结合自身场景实现自身的业务功能。
ReadWriteLock
读写锁的应用场景可能类似与多线程情况下,某种数据的线程安全场景下使用,一写多读,并且读锁可以升级写锁,这里读写锁代码因为有些长,文末我会附上Gitee地址,大家可以下载看看,我是写了一个链表去进行一写多读,
private readonly ReaderWriterLockSlim readerWriterLockSlim = new ReaderWriterLockSlim();//有参构造函数为支持递归的方式还是不支持递归的方式,用于指定锁定递归策略。
这样我们便构造了一个读写锁的实例,参数是使用什么策略去初始化读写锁,对于递归策略,在升级锁中是不建议使用递归的方式,因为可能会造成死锁的问题,如果是读取过程中使用递归的方式可能会造成LockRecursionException 异常;
对此,官网给出的解释是:
-
处于读取模式的线程可以以递归方式进入读取模式,但不能进入写入模式或可升级模式。 如果尝试执行此操作,则 LockRecursionException 会引发。 进入读取模式,然后进入写入模式或可升级模式是一种具有极大的死锁概率的模式,因此不允许这样做。 如前文所述,可升级模式适用于需要升级锁定的情况。
-
处于可升级模式的线程可以进入写入模式和/或读取模式,并且可以递归输入三种模式中的任何一种。 但是,如果有其他线程处于读取模式,则尝试进入写入模式块。
-
处于写入模式的线程可以进入读取模式和/或可升级模式,并且可以递归输入三种模式中的任何一种。
-
未进入锁定状态的线程可以进入任何模式。 尝试输入非递归锁的原因与此尝试相同。
而且,每次进入锁的时候在代码的最后都要去进行退出锁。
SpinLock
自旋锁,实际上我的理解可能也不够深,只是看官网的解释是不适用于阻塞的情况下,以及分配内存等,实际上按照理解,线程不会阻塞而是一直在通过循环旋转去尝试获取锁,那实际上性能方面如果时间长情况下会出现问题,所以并不适用于阻塞的情况使用,
SpinLock 和Lock相比,SpinLock 更适合共享资源的非耗时操作,如果耗时,并且阻塞的情况下会导致无法进行自旋,造成死锁,并且锁内部最好别造成阻塞,造成阻塞性能会劣于Lock,详细查看MSDN。
var sl = new SpinLock(false); foreach (var item in Enumerable.Range(0, 100)) { Thread thread = new Thread(() => { Stopwatch stopwatch = new Stopwatch(); var isEnter = false; sl.TryEnter(ref isEnter); if (isEnter) { stopwatch.Start(); var i = item; Queue.Enqueue(i); sl.Exit(false); stopwatch.Stop(); Console.WriteLine("SpinLock:" + stopwatch.ElapsedMilliseconds); } }); thread.Name = item.ToString(); thread.Start(); } foreach (var item in Enumerable.Range(0, 100)) { Thread thread = new Thread(() => { lock (_lock) { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); var i = item; Queue.Enqueue(i); stopwatch.Stop(); Console.WriteLine("Lock:" + stopwatch.ElapsedMilliseconds); } }); thread.Name = item.ToString(); thread.Start
此处的并发场景是多个线程去执行业务逻辑时,步骤可能一样的情况下,在每一步每一步都完成之后在继续开始执行下一步,即在赛跑的场景下,我们可以分为跑,以及颁奖两个步骤,那颁奖必须在所有运动员都完成跑步的情况下才会进行颁奖,那此处的场景就是都结束跑步才去进行下一步的操作。c#中有一个专门用来控制这样场景的类叫Barrier,官网给出的解释我觉得可能更贴切,使多个任务能够采用并行方式依据某种算法在多个阶段中协同工作。它的构造函数有两个参数一个是参与者的数量以及每一个在到达某一步骤之后需要进行的委托,传入的是Barrier实例。
//在定义三个屏障参与者之后,待所有参与者都到达屏障,继续执行后续操作, var barrier = new Barrier(3, s => { Console.WriteLine("这是第" + s.CurrentPhaseNumber + "次"); });//类似 三个任务 每个任务有三个阶段,也可以3个任务有三个阶段,也可以三个任务都只有一个阶段 var test = new BarrTest(); var random = new Random(); barrier.AddParticipant();//添加一个参与者 Action action = () => { Console.WriteLine("第一阶段开始");//第一阶段开始 barrier.SignalAndWait();//第一阶段到达屏障 Console.WriteLine("第一阶段完成");//三个第一阶段完成之后才可以执行这一句 #region 多任务可以有一个阶段,也可以多任务多阶段 Console.WriteLine("第二阶段开始");//第二阶段开始 barrier.SignalAndWait();// 第二阶段到达屏障 Console.WriteLine("第二阶段完成");//三个第二阶段完成之后才可以执行这一句 Console.WriteLine("第三阶段开始");//第三阶段开始 barrier.SignalAndWait();// 第三阶段到达屏障 Console.WriteLine("第三阶段完成");//三个第三阶段完成之后才可以执行这一句 #endregion }; for (int i = 0; i < 4; i++) { Task.Run(action);//三个任务去执行某一个任务,任务有三个阶段,每个阶段在上一阶段完成之后才可以继续执行 }
可以看到我们定义了三个参与者,那每一个参与者在完成之后都要向Barrier发出信号告知我们完成了这一步骤,在步骤完成之后我们在进行下一步骤,这里的操作实际上是4*4,就是我们启动了四个线程,每个线程执行的部分有包括了四个阶段,当然没我们也可以1*4,
在这个代码中将Region部分注释掉,即23阶段注释掉,然后我们将4改为1,然后Task.run(Action)分别run4次,这样我们实现了步骤场景下控制步骤执行的先后顺序并且,要求每一步到达之后才能继续下一步。
自定义任务调度
接下来我认为是到了重头戏哈哈哈,众所周知,c#线程的发展历程是thread,threadpool,然后是task,那实际上task也是基于线程池实现的调度,对线程池的资源有个合理的安排和调度使用,并且在线程控制以及回调方面都有一个很好的封装,实际上task都是基于taskschduler的抽象类去进行调度的,这个类是一个抽象类,c#中默认的实现的调度是threadpoolscheduler类去进行执行task方面的调度和运行,这个类里面有三个抽象方法,分别是获取队列的task,以及移除task添加task还有是否可以同步执行task的方法。
接下来我们看自己实现的任务调度以及如何使用
var scheduler = new TaskCustomScheduler(); var factory = new TaskFactory(scheduler); foreach (var item in Enumerable.Range(0, 10)) { factory.StartNew(() => { var i = item; Console.WriteLine(i); }); }
可以看到我们在初始化TaskFactory的时候需要传入自定义的调度,然后factorystartnew的时候这实际上就是一个task,他会执行到QueueTask方法中将Task添加进去,然后我们会使用ThreadPool去执行这个task,在执行结束之后我们又将这个Task移除掉,实际上自定义调度我们还可以控制实现一个限制数量的一个任务调度。
public class TaskCustomScheduler : TaskScheduler { private SpinLock SpinLock = new SpinLock(); public TaskCustomScheduler() { } private ConcurrentQueue<Task> Tasks = new ConcurrentQueue<Task>(); protected override IEnumerable<Task> GetScheduledTasks() { return Tasks.ToList(); } protected override void QueueTask(Task task) { Tasks.Enqueue(task); RunWork(); } protected override bool TryDequeue(Task task) { return Tasks.TryDequeue(out task); } protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { return TryExecuteTask(task); } private void RunWork() { ThreadPool.UnsafeQueueUserWorkItem(_ => { try { foreach (var item in Tasks) { var task = item; var isEnter = false; SpinLock.TryEnter(ref isEnter); TryExecuteTask(task); if (isEnter) { TryDequeue( task); SpinLock.Exit(false); } } } finally { } }, null); } }
自定义Awaiter
在本文中我们会对await关键字做讲解,async和await的我们会放到下一篇进行详细讲解,包括自定义状态机,以及IL代码的状态机转为c#代码是什么样子,实际上细心的同学会发现,await关键字之所以可以await 是因为有TaskAwaiter这个结构体,那对应yield也有一个YieldAwaiter的结构体,他们都有一个共同点就是实现了两个接口,分别是ICriticalNotifyCompletion, INotifyCompletion这两个接口一个是安全一个是非安全的,第一个接口又继承于第二个接口,他只有一个OnCompleted方法传入一个委托类型的参数,至于这个委托指向那个方法,可能很多人知道这个方法传入的是什么,这里留给大家一个疑问,下一篇关于多线程异步的我们会做讲解,
public class CustomAwaiter : ICriticalNotifyCompletion, INotifyCompletion { public CustomAwaiter(Func<int, int, string> obj) { Obj = obj; } private bool bIsFinesh; private Timer Timer { get; set; } public bool IsCompleted { get { return bIsFinesh; } } private SpinLock SpinLock = new SpinLock(); private string Result { get; set; } public Func<int, int, string> Obj { get; } public void OnCompleted(Action continuation) { Timer = new Timer(s => { var action = s as Action; var bIsEnter = false; SpinLock.TryEnter(ref bIsEnter); if (bIsEnter) { Result = Obj.Invoke(5, 10); SpinLock.Exit(false); } Thread.Sleep(5000); action?.Invoke(); bIsFinesh = true; }, continuation, 5000, int.MaxValue); } public void UnsafeOnCompleted(Action continuation) { Timer = new Timer(s => { var action = s as Action; var bIsEnter = false; SpinLock.TryEnter(ref bIsEnter); if (bIsEnter) { Result = Obj.Invoke(5, 10); SpinLock.Exit(false); } Thread.Sleep(5000); action?.Invoke(); bIsFinesh = true; }, continuation, 5000, int.MaxValue); } public string GetResult() { return Result; } }
可以看到,我们实现了这两个接口,可能大家奇怪GetResult是什么意思,大家都知道TaskAwaiter是有getResult方法的,那YieldAwaiter实际上也有这个方法,这个方法实际上代表你的task执行结束之后的一个结果,但是你集成这两个接口的时候 是不会自动有这个方法的 需要你们自己去写一个GetResult方法,除此之外,TaskAwaiter和Yield的也有一个GetAwaiter方法,他们内部的这个方法不是一个静态方法,但是如果我们实现自定义的情况下是需要有一个 拓展方法叫GetAwaiter方法,返回我们自定义的Awaiter。
public static CustomAwaiter GetAwaiter(this Func<int, int, string> obj) { return new CustomAwaiter(obj); }
foreach (var item in Enumerable.Range(0, 5)) { Task.Run(async () => { var i = item; var ts = new Func<int, int, string>((s, b) => { return Guid.NewGuid().ToString(); }); //var t= await ts; var tash = new TaskCustomScheduler(); var factory = new TaskFactory(tash); await factory.StartNew(async () => { var t = await ts; Console.WriteLine(t); }); }); }
可以看到,上面的代码我们使用自定义的任务调度以及自定义的await去等待Func类型的,获取到结果然后我们去进行输出。
总结
对于多线程这里,我也只是浅显的入门,很多地方我也有点糊涂,所以有不对的地方,希望各位能够指正,多线程方面的代码和表达式代码我已上传到网盘,有需要的可以下载,如果有疑问的可以在各个net群里看有没有叫四川观察的,那个就是我,或者加群6406277也可以找到我,
链接:https://pan.baidu.com/s/1XdDRkOCDP0mETMYp5d_-xg
提取码:1234