【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:是一个抽象类,我们一般不直接用,而是用它的派生类:
  1. EventWaitHandle:一旦创建,命名事件就对所有进程中的全部线程可见。 因此,命名事件可用于同步进程和线程的活动。事件等待句柄不是 .NET 事件。 并不涉及任何委托或事件处理程序。 之所以使用“事件”一词是因为它们一直都被称为操作系统事件,并且向等待句柄发出信号可以向等待线程指明事件已发生。
  2. AutoResetEvent:只能表示本地等待句柄。 无法表示命名系统事件。
  3. ManualResetEvent:只能表示本地等待句柄。 无法表示命名系统事件。
  4. Mutex 互斥量:跨进程同步
  5. 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();
    }
}

 

posted @ 2022-01-14 09:00  小林野夫  阅读(2153)  评论(0编辑  收藏  举报
原文链接:https://www.cnblogs.com/cdaniu/