【C# 线程】WaitHandle类
阅读原文时间:2023年07月09日阅读:1

Windows的线程同步方式可分为2种,用户模式构造和内核模式构造。
内核模式构造:是由Windows系统本身使用,内核对象进行调度协助的。内核对象是系统地址空间中的一个内存块,由系统创建维护。
  内核对象为内核所拥有,而不为进程所拥有,所以不同进程可以访问同一个内核对象, 如进程,线程,作业,事件(不是那个事情),文件,信号量,互斥量等都是内核对象。
  而信号量,互斥体,事件是Windows专门用来帮助我们进行线程同步的内核对象。
  对于线程同步操作来说,内核对象只有2个状态, 触发(终止,true)、未触发(非终止,false)。 未触发不可调度,触发可调度。

内核模式需要将托管代码转化为用户代码,然后切换成内核代码,所有很浪费时间
用户模式构造:是由特殊CPU指令来协调线程,上节讲的volatile实现就是一种,Interlocked也是。  也可称为非阻塞线程同步。

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回收也不会释放该对象的。

等待器,等待带事件发生。事件内部维护一个Boolean变量。事件为false,在事件上等待的线程就阻塞;事件为true,就能解除阻塞。

false就关闭砸门,true就是打开砸门。

和事件(AutoResetEvent类、ManualResetEvent类)配合使用。当这些事件调用set()方法时候,等待器就会收到信号。
具体过程:
创建一个等待事件(ManualResetEvent对象)
注册事件,waitthandle.waitany(事件);
当你在线程中执行完操作后,会告诉 WaitHandle"我已完成“ 事件. Set()。

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();  
}  

}

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章