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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM