.NET简谈组件程序设计之(手动同步)
在上一篇文章“.NET简谈组件程序设计之(上下文与同步域) ”中,我们学习了关于一些上下文和同步域的概念,可以利用这两个技术来进行自动同步。
今天我们主要学习怎么手动来执行同步,能从更小的粒度进行封锁,以达到最大程度的吞吐量。[王清培版权所有,转载请给出署名]
我们知道线程是进程的运行实体,进程是资源分配单位,而线程是执行单位。照书上所说,线程是程序的执行路径,当我们分配一个线程的时候,要确定线程的执行路径是什么,也就是代码中的ThreadStart委托所指向的入口点方法。
一旦我们手动Start启动线程的时候,当前上下文线程就被系统立即切换,我们从Thread.CurrentThread静态属性中可以准确的获取到当前正在执行的线程实例,然后进行一系列的设置、等待、终止。[王清培版权所有,转载请给出署名]
那么线程到底是怎么实现同步(互斥)的呢,在我们正常的代码中是没有关于线程同步的现实代码的,所以在线程执行路径中是不存在任何能够阻塞线程的实现代码。要想实现线程阻塞,就必须在线程的执行路径中写点东西,让所有线程当进入这段代码的时候(也就是临界资源),通过判断某种东西来确定是否允许进入执行。
图1:
在ThreadStartEnterPoint方法中,如果没有任何的同步代码,那么任何线程都能进去执行,就导致了乱七八糟的数据。当数据在内存中的时候,在同一时间只能是由CPU去执行线程的代码,但是线程是有竞争情况的,当线程1还没有完全执行完毕,线程2就来执行这块数据,导致数据的不同步。
那么我们需要再线程1还没有执行完毕前不允许其他线程使用这块内存对象。当线程1使用完后就立即释放所占有的资源,让其他线程能竞争。
利用Monitor(监视器)来进行同步
Monitor是用来提供同步的对象,通过它可以在某个时间点上锁定对象。请看代码:
[MethodImpl(MethodImplOptions.Synchronized)] public void ShowMessage() { for ( int i = 0; i < 10; i++) { if (i == 5) Monitor.Wait( this ); //等第二个线程使用完后,我在继续执行。将当前线程放置在等待队列里 Thread currentthread = Thread.CurrentThread; Console.WriteLine(currentthread.Name + "|" + currentthread.ManagedThreadId + "|" + i.ToString()); } } [MethodImpl(MethodImplOptions.Synchronized)] public void PluseThread() { for ( int i = 0; i < 10; i++) { Thread currentthread = Thread.CurrentThread; Console.WriteLine(currentthread.Name + "|" + currentthread.ManagedThreadId + "|" + i.ToString()); } Monitor.Pulse( this ); //将等待队列里的线程放到锁定队列,也就是Monitor.Enter(); } |
这是一个类中的两个方法,在方法的头部我用了MethodImpl方法特性进行了标识,其实这个特性的目的就是在方法的入口处和结束处加上同步方法,也就是Monitor.Enter和Monitor.Exit,一般情况下我们都是习惯用lock来锁定对象,其实lock也是Monitor的变体。这里我就不写出来了。让我们熟悉一下陌生的使用方式。
Myclass1 myclass = new Myclass1(); ThreadStart startdeleted = new ThreadStart(myclass.ShowMessage); Thread thread = new Thread(startdeleted); thread.Name = "线程1" ; ThreadStart stra = new ThreadStart(myclass.PluseThread); Thread thread2 = new Thread(stra); thread2.Name = "线程2" ; thread.Start(); thread2.Start(); thread2.Join(); Console.WriteLine( "线程2已经释放" ); thread.Join(); Console.WriteLine( "线程1已经释放" ); Console.WriteLine(Thread.CurrentThread.ManagedThreadId); Console.ReadLine(); |
在调用的代码里面,大概意思是这样的,我们同时开启两个线程,入口点分别是上面的两个方法,在PluseThread里面是为了将ShowMessage线程1从等待队列里释放出来继续执行。[王清培版权所有,转载请给出署名]
在ShowMessage里面我用Monitor.Wait方法等待,当调用这个方法的时候会使用我锁定的对象,让其他线程进入执行。当Monitor.Pluse的时候,线程1继续执行。
静态Monitor对象是每个线程都会执行的路径,我们通过控制Monitor来进行线程同步,当我们调用Wait就是等待,直到当前对象Pluse才继续执行。
图2:
利用WaitHandle(等待句柄)来进行同步
上面我们通过Monitor来进行同步,在同步的时候我们需要很好的控制等待时间,用Monitor也能通过Wait进行等待超时设置,也许它内部封装是Windows等待句柄。
这里我们通过使用WaitHandle来进行同步,WaitHandle是个抽象类,它的子类有很多,比如Mutex互斥体、ManualResetEvent、AutoResetEvent事件对象,等等。下面我们就来看看利用这些对象怎么同步线程。
Mutext互斥体
public void Print() { Mutex mutex = new Mutex( false , "myclass" ); //Mutex互斥体 mutex.WaitOne(); for ( int i = 0; i < 10; i++) { Console.WriteLine(i.ToString() + Thread.CurrentThread.Name); } mutex.ReleaseMutex(); } |
在方法的内部我们申请一个Mutex对象,这个Mutex是全局的,就是在一台机器上只能存在一个名称的Mutex,Mutex可用来同步线程也可以用来同步进程。
我们在Print方法里面用WaitOne获取句柄,如果已经有线程“捷足先得”了,那么这里将阻塞,并返回false。
在使用完后,记得调用ReleaseMutex释放当前占用的Mutex句柄。
Myclass1 myclass = new Myclass1(); Thread thread = new Thread( new ThreadStart(myclass.Print)); thread.Name = "线程1" ; Thread thread2 = new Thread( new ThreadStart(myclass.Print)); thread2.Name = "线程2" ; thread.Start(); thread2.Start(); thread2.Join(); thread.Join(); Console.WriteLine(Thread.CurrentThread.ManagedThreadId); |
图3:
利用ManualResetEvent(手动事件)来进行同步
我们直接看代码吧,ManualResetEvent大家可能都用过。
public class EventWaitHandlerDemo { ManualResetEvent resetevent = new ManualResetEvent( false ); public EventWaitHandlerDemo() { Thread thread = new Thread( new ThreadStart( this .DoWork)); thread.Start(); } private void DoWork() { int count = 0; while ( true ) { resetevent.WaitOne(); Console.WriteLine(count++); } } public void GoThread() { resetevent.Set(); Console.WriteLine( "线程启动" ); } public void Stopthread() { resetevent.Reset(); Console.WriteLine( "线程暂停" ); } public void Close() { resetevent.Close(); Console.WriteLine( "线程终止" ); } } |
这种类型的等待句柄对象是完全手动控制的,让我们想要用的时候要记得set,想要暂停的时候就Reset,不用了就close。
EventWaitHandlerDemo demo = new EventWaitHandlerDemo(); demo.GoThread(); Thread.Sleep(1000); demo.Stopthread(); Thread.Sleep(1000); demo.Close(); Console.WriteLine( "程序结束" ); |
图4:
利用AutoResetEvent(自动事件)来进行同步
从名字上就能看出,该事件是自动重置事件,不需要想上面那样进行set\reset操作。
public class AutoResetEventDemo { AutoResetEvent autoevent = new AutoResetEvent( true ); public AutoResetEventDemo() { } public void Print() { autoevent.WaitOne(); // autoevent.Reset();在手动事件中,需要手动切换状态 int i = 0; while ( true ) { if (i == 10) { Console.WriteLine(Thread.CurrentThread.Name + "线程结束" ); autoevent.Set(); break ; } Console.WriteLine(Thread.CurrentThread.ManagedThreadId + "|" + i++); } } } |
在上面的代码中,我们通过WaitOne获取等待句柄,当我们获取到之后,事件对象会自动重置为信号已发,其他线程无法获取到等待句柄。当我们set之后其他线程才能获取到,这里省掉的是线程进入执行路径的过程。
ManualResetEvent需要手动进行set才能使用,一旦set之后信号标记为未发状态,所有线程都能执行代码,除非手动Reset才能阻塞。
Console.WriteLine(Thread.CurrentThread.ManagedThreadId); AutoResetEventDemo autodemo = new AutoResetEventDemo(); Thread thread1 = new Thread( new ThreadStart(autodemo.Print)); thread1.Name = "线程1" ; Thread thread2 = new Thread( new ThreadStart(autodemo.Print)); thread2.Name = "线程2" ; thread1.Start(); thread2.Start(); thread1.Join(); thread2.Join(); Console.WriteLine(Thread.CurrentThread.ManagedThreadId); Console.Read(); |
图5:
[王清培版权所有,转载请给出署名]
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!