【C# 线程】WaitHandle类
理论
Windows的线程同步方式可分为2种,用户模式构造和内核模式构造。
内核模式构造:是由Windows系统本身使用,内核对象进行调度协助的。内核对象是系统地址空间中的一个内存块,由系统创建维护。
内核对象为内核所拥有,而不为进程所拥有,所以不同进程可以访问同一个内核对象, 如进程,线程,作业,事件(不是那个事情),文件,信号量,互斥量等都是内核对象。
而信号量,互斥体,事件是Windows专门用来帮助我们进行线程同步的内核对象。
对于线程同步操作来说,内核对象只有2个状态, 触发(终止,true)、未触发(非终止,false)。 未触发不可调度,触发可调度。
内核模式需要将托管代码转化为用户代码,然后切换成内核代码,所有很浪费时间
用户模式构造:是由特殊CPU指令来协调线程,上节讲的volatile实现就是一种,Interlocked也是。 也可称为非阻塞线程同步。
WaitHandle类的刨析
aitHandle是C#编程中等待和通知机制的对象模型。
在windows编程中,通过API创建一个内核对象后会返回一个句柄,句柄则是每个进程句柄表的索引,而后可以拿到内核对象的指针、掩码、标示等。
而WaitHandle抽象基类类作用是包装了一个windows内核对象的句柄。我们来看下其中一个WaitOne的函数源码(略精简)。
System.Threading命名空间中提供了一个WaitHandle 的抽象基类
public abstract partial class WaitHandle : MarshalByRefObject, IDisposable { 。。。。。。。
EventWaitHandle 事件等待句柄不是 .NET 事件。 并不涉及任何委托或事件处理程序。 之所以使用“事件”一词是因为它们一直都被称为操作系统事件,并且向等待句柄发出信号可以向等待线程指明事件已发生。
IDisposable:继承该接口要用using,和try catch 即使释放。
MarshalByRefObject:分为Marshal、ByRefObject。在 .NET Remoting 中,不论是传值或传址,每一个对象都必须要继承 System.MarshalByRefObject 类别,才可以利用 .NET Remoting 来传输。该类可以穿越同步域、线程、appdomain、进程
Marshal:类似于序列化。
ByRefObject:传递对象引用(类似于文件的快捷方式)。
.NET Remoting:
.NET Remoting 是一项传统技术,保留该技术是为了向后兼容现有的应用程序,不建议对新的开发使用该技术。现在应该使用 Windows Communication Foundation (WCF) 来开发分布式应用程序。
.NET Remoting是微软随.NET推出的一种分布式应用解决方案,被誉为管理应用程序域之间的 RPC 的首选技,它允许不同应用程序域之间进行通信(这里的通信可以是在同一个进程中进行、一个系统的不同进程间进行、不同系统的进程间进行)。
更具体的说,Microsoft
.NET Remoting 提供了一种允许对象通过应用程序域与另一对象进行交互的框架。也就是说,使用.NET
Remoting,一个程序域可以访问另外一个程序域中的对象,就好像这个对象位于自身内部,只不过,对这个远程对象的调用,其代码是在远程应用程序域中进行的,例如在本地应用程序域中调用远程对象上一个会弹出对话框的方法,那么,这个对话框,则会在远程应用程序域中弹出。
即使是在同一个Domain里,但如果是在不同的Context中,也需要继承MarshalByRefObject才能访问
通过以上分析我们得出WatiHandle是一个可远程调用的对象。一般的对象只能在本地应用程序域之内被引用,而MarshalByRefObject对象可以跨越应用程序域边界被引用,甚至被远程引用。
远程调用时,将产生一个远程对象在本地的透明代理,通过此代理来进行远程调用。
特别注意:
MarshalByRefOjbect当被远端调用时候,通过生命期服务 LifeService 控制该结构的生命周期。默认情况下,如果不再有任何调用操作后大约15分钟将销毁该结构。
所以强制GC回收也不会释放该对象的。
WatiHandle是什么?有什么用
等待器,等待带事件发生。事件内部维护一个Boolean变量。事件为false,在事件上等待的线程就阻塞;事件为true,就能解除阻塞。
false就关闭砸门,true就是打开砸门。
和事件(AutoResetEvent类、ManualResetEvent类)配合使用。当这些事件调用set()方法时候,等待器就会收到信号。
具体过程:
创建一个等待事件(ManualResetEvent对象)
注册事件,waitthandle.waitany(事件);
当你在线程中执行完操作后,会告诉 WaitHandle"我已完成“ 事件. Set()。
派生类
- WaitHandle:是一个抽象类,我们一般不直接用,而是用它的派生类:
- EventWaitHandle:一旦创建,命名事件就对所有进程中的全部线程可见。 因此,命名事件可用于同步进程和线程的活动。事件等待句柄不是 .NET 事件。 并不涉及任何委托或事件处理程序。 之所以使用“事件”一词是因为它们一直都被称为操作系统事件,并且向等待句柄发出信号可以向等待线程指明事件已发生。
- AutoResetEvent:只能表示本地等待句柄。 无法表示命名系统事件。
- ManualResetEvent:只能表示本地等待句柄。 无法表示命名系统事件。
- Mutex 互斥量:跨进程同步
- Semaphore 信号量:跨进程同步
WaitHandle类 成员
字段
InvalidHandle:可以使用此值确定Handle属性是否包含有效的本机操作系统句柄。
WaitTimeout:是个常数258。是waitAny的返回值之一,如果超时就返回258。
属性
Handle 已过时
SafeWaitHandle:一个代表本地操作系统句柄的SafeWaitHandle。不要手动关闭该句柄,因为当SafeWaitHandle试图关闭该句柄时,会导致ObjectDisposedException异常。
方法
Close:关闭当前WaitHandle持有的所有资源。WaitHandle 持有很多对象的句柄。必须关闭后才能释放
Dispose:释放当前WaitHandle对象。
WaitHandle.WaitAll
只能在多线程运行。在指定的时间内(-1表示无限等待,或者具体的时间)一直等待。直到收到所有的子线程发出set信号或主线程等待超时。
WaitAll():
基于WaitmultipleObject,只支持MTAThreadAttribute 的线程,实现要比WaitSingleObject复杂的多,性能也不好,尽量少用。在传给WaitAny()和WaitAll()方法的数组中,包含的元素不能超过64个,否则方法会抛出一个System.NotSupportedException。
并且与旧版 COM 体系结构有奇怪的连接:这些方法要求调用方位于多线程单元中,该模型最不适合互操作性。
例如,WPF 或 Windows 应用程序的主线程无法在此模式下与剪贴板进行交互。我们稍后将讨论替代方案。SignalAndWait
WaitHandle.WaitAny
只能在多线程运行。在指定的时间内(-1表示无限等待,或者具体的时间)一直等待。直到收到任意一个的子线程发出set信号或主线程等待超时。
WaitAny():
基于WaitmultipleObject,只支持MTAThreadAttribute 的线程,实现要比WaitSingleObject复杂的多,性能也不好,尽量少用。如果没有任何对象满足等待,并且WaitAny()设置的等待的时间间隔已过,则为返回WaitTimeout。在传给WaitAny()和WaitAll()方法的数组中,包含的元素不能超过64个,否则方法会抛出一个System.NotSupportedException。
WaitHandle.WaitOne
单线程或者多线程中,在指定的时间内(-1表示无限等待,或者具体的时间)一直等待。直到收到set信号或等待超时。
WaitOne():事件内部维护一个Boolean变量。事件为false,在事件上等待的线程就阻塞;事件为true,就能解除阻塞。
基于WaitSingleObject 阻止当前线程,直到当前 WaitHandle 收到信号。
WaitOne(Int32 time) :
阻止当前线程,计时等待,在规定time之前收到信号,就返回true。否则返回false。-1表示无限等待。
WaitOne(Int32, Boolean):
在学习这个方法之前,你必须储备这些知识:NET Remoting 通信模型、MarshalByRefObject类、上下文绑定、同步域等概念。才会理解该方法的用法。
Int32:等待时间
Boolean:是否在执行waitone方法之前 退出同步上下文(因为此时waithandle捕获了同步域的上下文,只有当前线程退出后其他线程才能进入同步域),false 那么其他线程在当前线程执行waitone方法未超时期间无法进入同步域。true 其他线程可在当期线程等待期间可用进入。
除非从【非默认的托管上下文(指同步域的上下文)】中调用WaitOne方法,否则exitContext参数没有作用。默认的托管上下文就是AppDomain建立后就会建立一个默认的托管上下文。
当您的代码在非默认上下文中执行时,为exitContext指定true将导致线程在执行WaitOne方法之前退出非默认的托管上下文中(即转换到默认上下文中)。在对WaitOne方法的调用完成后,线程返回到原始的非默认上下文。
当上下文绑定类具有SynchronizationAttribute时,这可能很有用。在这种情况下,对类成员的所有调用都自动同步,同步域是类的整个代码体。如果成员的调用堆栈中的代码调用WaitOne方法,并为exitContext指定true,则线程退出同步域,从而允许在调用对象的任何成员时被阻塞的线程继续进行。当WaitOne方法返回时,进行调用的线程必须等待重新进入同步域。
您可以在任何 ContextBoundObject 子类上使用SynchronizationAttribute来同步所有实例方法和字段。同一上下文域中的所有对象共享同一锁。允许多个线程访问方法和字段,但一次只允许一个线程。
案例如下:
案例一、
using System; using System.Threading; using System.Runtime.Remoting.Contexts; [Synchronization(true)]//允许线程重入 public class SyncingClass : ContextBoundObject { private EventWaitHandle waitHandle; public SyncingClass() { waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset); } public void Signal() { Console.WriteLine("Thread[{0:d4}]: Signalling...", Thread.CurrentThread.GetHashCode()); waitHandle.Set(); } public void DoWait(bool leaveContext) { bool signalled; waitHandle.Reset(); Console.WriteLine("Thread[{0:d4}]: Waiting...", Thread.CurrentThread.GetHashCode()); signalled = waitHandle.WaitOne(3000, leaveContext); if (signalled) { Console.WriteLine("Thread[{0:d4}]: Wait released!!!", Thread.CurrentThread.GetHashCode()); } else { Console.WriteLine("Thread[{0:d4}]: Wait timeout!!!", Thread.CurrentThread.GetHashCode()); } } } public class TestSyncDomainWait { public static void Main() { SyncingClass syncClass = new SyncingClass(); Thread runWaiter; Console.WriteLine("\nWait and signal INSIDE synchronization domain:\n"); runWaiter = new Thread(RunWaitKeepContext); runWaiter.Start(syncClass); Thread.Sleep(1000); Console.WriteLine("Thread[{0:d4}]: Signal...", Thread.CurrentThread.GetHashCode()); // This call to Signal will block until the timeout in DoWait expires. syncClass.Signal(); runWaiter.Join(); Console.WriteLine("\nWait and signal OUTSIDE synchronization domain:\n"); runWaiter = new Thread(RunWaitLeaveContext); runWaiter.Start(syncClass); Thread.Sleep(1000); Console.WriteLine("Thread[{0:d4}]: Signal...", Thread.CurrentThread.GetHashCode()); // This call to Signal is unblocked and will set the wait handle to // release the waiting thread. syncClass.Signal(); runWaiter.Join(); } public static void RunWaitKeepContext(object parm) { ((SyncingClass)parm).DoWait(false); } public static void RunWaitLeaveContext(object parm) { ((SyncingClass)parm).DoWait(true); } } // The output for the example program will be similar to the following: // // Wait and signal INSIDE synchronization domain: //因为线程4 未退出同步域,所以线程1一无法进入。只能等线程4超时后,线程1才能进入。 // Thread[0004]: Waiting... // Thread[0001]: Signal... // Thread[0004]: Wait timeout!!! // Thread[0001]: Signalling... // // Wait and signal OUTSIDE synchronization domain: //因为线程6,在执行waitone()之前已经退出同步域(回到默认域),所以线程1可用在线程6等待期间,进入同步域内执行方法Signal() // Thread[0006]: Waiting... // Thread[0001]: Signal... // Thread[0001]: Signalling... // Thread[0006]: Wait released!!!
案例二、
使用SynchronizationAttribute和ContextBoundObject一起组合创建一个简单的自动的同步。
该对象内部构成一个同步域。只允许一个线程进入。
将 SynchronizationAttribute应用于某个类后,该类的实例无法被多个线程同时访问。我们说,这样的类是线程安全的。
该方式实现的同步已经过时,只做了解。
using System;
using System.Threading;
using System.Runtime.Remoting.Contexts;
[Synchronization]//不允许线程重入
public class AutoLock : ContextBoundObject
{
public void Demo()
{
Console.Write ("Start...");
Thread.Sleep (1000); // We can't be preempted here
Console.WriteLine ("end"); // thanks to automatic locking!
}
}
public class Test
{
public static void Main()
{
AutoLock safeInstance = new AutoLock();
new Thread (safeInstance.Demo).Start(); // Call the Demo
new Thread (safeInstance.Demo).Start(); // method 3 times
safeInstance.Demo(); // concurrently.
}
}
输出:
Start... end Start... end Start... end
原因就是整个对象内部就是一个同步域(锁的临界区),就是在当前没处理完,其他线程是无法进行操作的。
CLR确保一次只有一个线程可以执行其中的代码。它通过创建一个同步对象来实现这一点——并在每个方法或属性的每次调用时锁定它。锁的作用域——在本例中同步对象——被称为同步上下文。safeinstancesafeinstance
那么,这是如何工作的呢?一个线索在属性的命名空间:。A可以被认为是一个“远程”对象,这意味着所有的方法调用都被拦截。为了使这种拦截成为可能,当我们实例化时,CLR实际上返回一个代理——一个具有与对象相同的方法和属性的对象,它充当中介。自动锁定就是通过这个中介发生的。总的来说,拦截在每个方法调用上增加了大约一微秒。。。点击查看内容来源
关于退出上下文的说明
除非从非默认的托管上下文中调用WaitOne方法,否则exitContext参数没有作用。如果你的线程在调用一个从ContextBoundObject派生的类实例时,就会发生这种情况。即使您当前正在执行一个不是从ContextBoundObject派生的类上的方法,如String,如果ContextBoundObject在当前应用程序域中的堆栈上,您也可以处于非默认上下文中。
当您的代码在非默认上下文中执行时,为exitContext指定true将导致线程在执行WaitOne方法之前退出非默认的托管上下文中(即转换到默认上下文中)。在对WaitOne方法的调用完成后,线程返回到原始的非默认上下文。
当上下文绑定类具有SynchronizationAttribute时,这可能很有用。在这种情况下,对类成员的所有调用都自动同步,同步域是类的整个代码体。如果成员的调用堆栈中的代码调用WaitOne方法,并为exitContext指定true,则线程退出同步域,从而允许在调用对象的任何成员时被阻塞的线程继续进行。当WaitOne方法返回时,进行调用的线程必须等待重新进入同步域。
WaitHandle.SignalAndWait
基于WaitmultipleObject,只支持MTAThreadAttribute 的线程。在指定的时间内(-1表示无限等待,或者具体的时间)一直等待。直到收到对应的子线程发出set信号或主线程等待超时。
SignalAndWait(WaitHandle ewh, WaitHandle clearCount)
默认无期限(-1)的等待子线程的返回信号。给ewh 释放一个set()信号,然后当前进程处于阻塞状态,切换到SignalAndWait所在的线程中执行,当子线程运行到 clearCount.Set();又切换到当前进程执行。 在具有 STAThreadAttribute 的线程中不支持 SignalAndWait ()方法。
SignalAndWait(WaitHandle, WaitHandle, Int32 time, Boolean)
time表示在子线程中等待N秒钟,如果未等到子线程的信号,就切回到SignalAndWait所在的线程,true表示退出子线程上下文。 这样其他线程就可用进入子线程的代码区。 进行执行在具有 STAThreadAttribute 的线程中不支持 SignalAndWait()方法。
SignalAndWait(WaitHandle, WaitHandle, TimeSpan, Boolean)
设定一个超时时间。在具有 STAThreadAttribute 的线程中不支持 SignalAndWait ()方法。
案例 运行轨迹如吓
using System; using System.Threading; public class Example { // The EventWaitHandle used to demonstrate the difference // between AutoReset and ManualReset synchronization events. // private static EventWaitHandle ewh; // A counter to make sure all threads are started and // blocked before any are released. A Long is used to show // the use of the 64-bit Interlocked methods. // private static long threadCount = 0; // An AutoReset event that allows the main thread to block // until an exiting thread has decremented the count. // private static EventWaitHandle clearCount = new EventWaitHandle(false, EventResetMode.AutoReset); [MTAThread] public static void Main() { // Create an AutoReset EventWaitHandle. // ewh = new EventWaitHandle(false, EventResetMode.AutoReset); for (int i = 0; i <= 4; i++) { Thread t = new Thread( new ParameterizedThreadStart(ThreadProc) ); t.Start(i); } // while (Interlocked.Read(ref threadCount) < 5) { Thread.Sleep(500); } // 当线程都处于阻塞后运行到这一步 while (Interlocked.Read(ref threadCount) > 0) { //给ewh 释放一个set()信号,然后当前进程处于阻塞状态,切换到子线程中执行,当子线程运行到 clearCount.Set();又切换到当前进程执行。 // 如果都完成了就返回true。 WaitHandle.SignalAndWait(ewh, clearCount); //2000表示在子线程中等待2s中,如果未等到子线程信号,就切回到主线程,true表示退出子线程上下文。 这样其他线程就可用进入代码区 进行执行 // WaitHandle.SignalAndWait(ewh, clearCount,2000,true); } Console.WriteLine(); // Create a ManualReset EventWaitHandle. // ewh = new EventWaitHandle(false, EventResetMode.ManualReset); // Create and start five more numbered threads. // for (int i = 0; i <= 4; i++) { Thread t = new Thread( new ParameterizedThreadStart(ThreadProc) ); t.Start(i); } // Wait until all the threads have started and blocked. // while (Interlocked.Read(ref threadCount) < 5) { Thread.Sleep(500); } Console.WriteLine("Press ENTER to release the waiting threads."); Console.ReadLine(); ewh.Set(); } public static void ThreadProc(object data) { int index = (int)data; Console.WriteLine("Thread {0} blocks.", data); // Increment the count of blocked threads. Interlocked.Increment(ref threadCount); //线程在这边阻塞等待 信号。 ewh.WaitOne(); Console.WriteLine("Thread {0} exits.", data); // Decrement the count of blocked threads. Interlocked.Decrement(ref threadCount); //这个执行完成后,会切回到主线程 // clearCount.Set(); } }