iT邦幫忙

0

[讀書筆記] Threading in C# - PART 3: USING THREADS

  • 分享至 

  • xImage
  •  

本篇同步發文於個人Blog: [讀書筆記] Threading in C# - PART 3: USING THREADS

The Event-Based Asynchronous Pattern

  • EAP可啟用多執行緒且不需要Consumer主動或管理thread, 有以下特徵
  1. 合作式取消模型

  2. 當Worker thread完成工作, 可安全更新WFP/Winform的UI元件

  3. 在完成的事件傳遞Exception

  • EAP只是個Pattern, 實作上最常見的有BackgroundWorker, WebClient等

  • 這些Class包含有*Async的方法, 通常就是EAP. 呼叫*Async的方法, 將任務交給其他thread執行, 而任務完成後, 會觸發Completed事件

  • *Completed事件的參數包含這些:

  1. 有個flag標示該任務是否有被取消

  2. 有exception拋出時, 包裝在Error物件

  3. call function代入的userToken

  • 使用EAP的設計, 如果有遵循APM的規則, 可以節省Thread

  • 之後的Task實作和EAP很相似, 讓EAP的魅力大減

BackgroundWorker

  • BackgroundWorker是在System.ComponentModel, 符合EAP設計, 並有以下特徵:
  1. 合作的取消模型

  2. 當Worker完成, 可以安全更新WPF/Winform的Control

  3. 把Exception傳遞到完成事件

  4. 有個進度回報的protocol

  5. 實作IComponent, 在Design time(Ex: Visual Studio Designer)可以被託管

Using BackgroundWorker

  • 建立BackgroundWorker的最小步驟: 建立BackgroundWorker並處理DoWork事件, 再呼叫RunWorkerAsync函式, 此函式也能代入參數. 在DoWork委託的函式, 從DoWorkEventArgs取出Argument, 代表有代入的參數.

  • 以下是基本的範例

    using System;
    using System.ComponentModel;
    
    namespace BackgroundWorkerTest
    {
        class Program
        {
            static BackgroundWorker _bw = new BackgroundWorker();
            static void Main(string[] args)
            {
                _bw.DoWork += MyDoWork;
                _bw.RunWorkerAsync(123456);
                Console.ReadLine();
            }
    
            private static void MyDoWork(object sender, DoWorkEventArgs e)
            {
                Console.WriteLine(e.Argument);
            }
        }
    }
  • BackgroundWorker有個RunWorkerCompleted事件, 當DoWork的事件完成將會觸發, 而在RunWorkerCompleted裡查詢有DoWork拋出的Exception、也能對UI Control做更新

  • 如果要增加progress reporting, 要以下步驟:

  1. 設置WorkerReportsProgress屬性為true

  2. 定期在DoWork的委託事件呼叫ReportProgress, 代入目前完成的進度值, 也可選代入user-state

  3. 新增ProgressChanged事件處理, 查詢前面代入的進度值, 用ProgressPercentage參數查

  4. 在ProgressChanged也能更新UI Control

  • 如果要增加Cancellation的功能:
  1. 設置WorkerSupportsCancellation屬性為true

  2. 定期在DoWork的委託事件內檢查CancellationPending這個boolean值, 如果它是true, 則可以設置DoWorkEventArgs的Cancel為true並做return. 如果DoWork的工作太困難而不能繼續執行, 也可以不理會CancellationPending的狀態而直接設Cancel為true

  3. 呼叫CancelAsync來請求取消任務

  • 以下是progress reporting和cancellation的範例, 每經過1秒會回報累加的進度(每次增加20). 如果在5秒內按下任何鍵, 會送出cancel請求並停止DoWork. 否則超過5秒後, 在DoWorkEventArgs的Result可以設值, 並在RunWorkerCompleted的RunWorkerCompletedEventArgs的Result取值.
    using System;
    using System.ComponentModel;
    using System.Threading;
    
    namespace BackgroundWorkerProgressCancel
    {
        class Program
        {
            static BackgroundWorker _bw;
            static void Main(string[] args)
            {
                _bw = new BackgroundWorker
                {
                    WorkerReportsProgress = true,
                    WorkerSupportsCancellation = true
                };
                _bw.DoWork += bw_DoWork;
                _bw.ProgressChanged += bw_ProgressChanged;
                _bw.RunWorkerCompleted += bw_RunWorkerCompleted;
    
                _bw.RunWorkerAsync("Run worker now");
    
                Console.WriteLine("Press Enter in the next 5 seconds to cancel");
                Console.ReadLine();
                if (_bw.IsBusy)
                {
                    _bw.CancelAsync();
                }
    
                Console.ReadLine();
            }
    
            private static void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
            {
                if (e.Cancelled)
                {
                    Console.WriteLine("You canceled");
                }
                else if(e.Error != null)
                {
                    Console.WriteLine("Worker exception: " + e.Error.ToString());
                }
                else
                {
                    Console.WriteLine("Completed: " + e.Result);
                }
            }
    
            private static void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
            {
                Console.WriteLine("Reached " + e.ProgressPercentage + "%");
            }
    
            private static void bw_DoWork(object sender, DoWorkEventArgs e)
            {
                for(int i = 0; i <= 100; i+= 20)
                {
                    if (_bw.CancellationPending)
                    {
                        e.Cancel = true;
                        return;
                    }
    
                    _bw.ReportProgress(i);
                    Thread.Sleep(1000);
                }
    
                e.Result = 123456;
            }
        }
    }

Subclassing BackgroundWorker

  • 可以繼承BackgroundWorker來實作EAP

  • 以下範例是整合前面BackgroundWorker的範例, 再搭配原作者未完整的繼承案例, 功能是每一秒會累加財務的金額和信用點數, 增加的值是建構物件時給的參數. 經過5秒後把累加的結果放在Dictionary

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Threading;
    
    namespace BackgroundWorkerSubClass
    {
        class Program
        {
            static FinancialWorker _bw;
            static void Main(string[] args)
            {
                _bw = new Client().GetFinancialTotalsBackground(10, 50);
                _bw.ProgressChanged += bw_ProgressChanged;
                _bw.RunWorkerCompleted += bw_RunWorkerCompleted;
    
                _bw.RunWorkerAsync("Hello to worker");
                Console.WriteLine("Press Enter in the next 5 seconds to cancel");
                Console.ReadLine();
                if (_bw.IsBusy) _bw.CancelAsync();
                Console.ReadLine();
            }
    
            static void bw_RunWorkerCompleted(object sender,
                                         RunWorkerCompletedEventArgs e)
            {
                if (e.Cancelled)
                    Console.WriteLine("You canceled!");
                else if (e.Error != null)
                    Console.WriteLine("Worker exception: " + e.Error.ToString());
                else
                {
                    Dictionary<string, int> result = e.Result as Dictionary<string, int>;
                    Console.WriteLine("Complete: ");      // from DoWork
                    foreach (var item in result)
                    {
                        Console.WriteLine($"Key {item.Key} Value {item.Value}");
                    }
                }
    
            }
    
            static void bw_ProgressChanged(object sender,
                                            ProgressChangedEventArgs e)
            {
                Console.WriteLine("Reached " + e.ProgressPercentage + "%");
            }
        }
    
        public class Client
        {
            public FinancialWorker GetFinancialTotalsBackground(int moneyIncreaseBase, int creditPointIncreaseBase)
            {
                return new FinancialWorker(moneyIncreaseBase, creditPointIncreaseBase);
            }
        }
    
        public class FinancialWorker : BackgroundWorker
        {
            public Dictionary<string, int> Result;   // You can add typed fields.
            public readonly int MoneyIncreaseBase, CreditPointIncreaseBase;
    
            public FinancialWorker()
            {
                WorkerReportsProgress = true;
                WorkerSupportsCancellation = true;
            }
    
            public FinancialWorker(int moneyIncreaseBase, int creditPointIncreaseBase) : this()
            {
                this.MoneyIncreaseBase = moneyIncreaseBase;
                this.CreditPointIncreaseBase = creditPointIncreaseBase;
            }
    
            protected override void OnDoWork(DoWorkEventArgs e)
            {
                Result = new Dictionary<string, int>();
                Result.Add("Money", 0);
                Result.Add("CreditPoint", 0);
    
                int percentCompleteCalc = 0;
                while (percentCompleteCalc <= 80)
                {
                    if (CancellationPending)
                    {
                        e.Cancel = true;
                        return;
                    }
                    ReportProgress(percentCompleteCalc, "Monet & Credit Point is increasing!");
                    percentCompleteCalc += 20;
                    Result["Money"] += MoneyIncreaseBase;
                    Result["CreditPoint"] += CreditPointIncreaseBase;
                    Thread.Sleep(1000);
                }
                ReportProgress(100, "Done!");
                e.Result = Result;
            }
        }
    }
  • 這種繼承寫法, 可以讓Caller不用指定DoWork委託, 在呼叫RunWorkerAsync時執行有override的OnDoWork.

  • 主要是把progress report, cancellation和comleted(可以更新UI之類、取運算結果)要負責的功能給caller指定, 而DoWork的邏輯交給該BackgroundWorker子類別負責.

Interrupt and Abort

  • Interrupt和Abort能停止Blocked的thread

  • Abort也能停止非block的thread, 比如一直在無限迴圈執行的thread, 所以Abort會在特定場合使用, 但Interrupt很少用到

Interrupt

  • Interrupt能強制使blocked thread釋放, 並拋出ThreadInterruptedException.

  • 除非沒有handle ThreadInterruptedException(Catch抓到它), 否則該thread在interrupt後不會結束.

    using System;
    using System.Threading;
    
    namespace InterruptBasic
    {
        class Program
        {
            static void Main(string[] args)
            {
                Thread t = new Thread(() => 
                {
                    try
                    {
                        Thread.Sleep(Timeout.Infinite);
                    }
                    catch (ThreadInterruptedException)
                    {
                        Console.WriteLine("Forcibly");
                    }
                    Console.WriteLine("Woken!");
                });
    
                t.Start();
                t.Interrupt();
            }
        }
    }
  • 如果對一個non-blocked的thread使用interrupt, 它仍會持續進行, 直到它blocked, 就會拋出ThreadInterruptedException. 以下範例呈現此功能, Main thread對worker thread做interrupt, 而worker執行完一個稍微久的迴圈再做Blocked(Sleep)就會拋exception
    using System;
    using System.Threading;
    
    namespace ThreadInterruptNonblocking
    {
        class Program
        {
            static void Main(string[] args)
            {
                Thread t = new Thread(() =>
                {
                    try
                    {
                        long count = 0;
                        while (count < 1000000000)
                        {
                            count++;
                        }
                        Console.WriteLine("Sleep");
                        Thread.Sleep(1000);
                        Console.WriteLine("I am done");
                    }
                    catch(ThreadInterruptedException ex)
                    {
                        Console.WriteLine("Catch interrupt!");
                    }
                });
    
                t.Start();
                Console.WriteLine("Call interrupt");
                t.Interrupt();
    
                Console.ReadLine();
            }
        }
    } 
  • 先確認thread的狀態再呼叫interrupt, 可以避免此問題, 但這方法不是thread-safe, 因為if 和 interrupt 會有機率發生搶占
    if ((worker.ThreadState & ThreadState.WaitSleepJoin) > 0)
      worker.Interrupt();
  • 只要有thread在lock或synchronized的時候發生blocked, 則有對它interrupt的指令蜂擁而上. 如果該thread沒有處理好發生interrupt後的後續(比如在finally要釋放資源), 將導致資源不正確釋放、物件狀態未知化.

  • 因此, interrupt是不必要的, 要強制對blocked thread做釋放, 安全的方式是用cancellation token. 如果是要unblock thread, Abort相較是比較有用的.

Abort (Net Core不支援)

  • Abort也是強制釋放blocked thread, 且會拋出ThreadAbortException. 但是在catch結尾會再重拋一次該exception

  • 如果有在catch呼叫Thread.ResetAbort, 就不會發生重拋

  • 在呼叫Abort後的時間內, 該thread的ThreadState是AbortRequested

  • 尚未handle的ThreadAbortException 並不會造成程式shutdown

  • Abort和Interrupt的最大差異在於被呼叫的non-blocked thread會發生什麼事. Interrupt會等到該thread block才運作, 而Abort會立即拋出exceptio(unmanaged code除外)

  • Managed code不是abort-safe, 比如有個FileStream在建構讀檔的過程被Abort, 而unmanaged的file handler沒被中止, 導致檔案一直open, 直到該程式的AppDomain結束才會釋放.

  • 有2個案例是可以安全做Abort:

  1. 在abort該thread後, 連它的AppDomain也要中止. 比如Unit testing

  2. 對自身Thread做Abort, 比如ASP.NET的Redirect機制是這樣做

  • 作者的LINQPad工具, 當取消某個查詢時, 對它的thread abort. abort結束後, 會拆解並重新建立新的application domain, 避免發生潛在受汙染狀態

Safe Cancellation

  • Abort在大部分的情境, 是個很危險的功能

  • 建議替代的方式是實作cooperative pattern, 也就是worker會定期檢查某個flag, 如果該flag被設立, 則自己做abort(比如BackgroundWorker)

  • Caller對該flag設置, Worker會定期檢查到.

  • 這種pattern的缺點是worker的method必須顯式支援cancellation

  • 這種是少數安全的cancellation pattern

  • 以下是自定義封裝的cancellation flag class:

    using System;
    using System.Threading;
    
    namespace CancellationCustom
    {
        class Program
        {
            static void Main(string[] args)
            {
                var canceler = new RulyCanceler();
                new Thread(()=>{
                    try{
                        Work(canceler);
                    }
                    catch(OperationCanceledException){
                        Console.WriteLine("Canceled");
                    }
                }).Start();
    
                Thread.Sleep(1000);
                canceler.Cancel();
            }
    
            private static void Work(RulyCanceler canceler)
            {
                while(true)
                {
                    canceler.ThrowIfCancellationRequested();
                    try
                    {
                        // other method
                        OtherMethod(canceler);
                    }
                    finally
                    {
                        // any required cleanup
                    }
                }
            }
    
            private static void OtherMethod(RulyCanceler canceler)
            {
                // do stuff...
                for(int i = 0 ; i < 1000000;++i)
                {
                }
                Console.WriteLine("I am doing work");
    
    
                canceler.ThrowIfCancellationRequested();
            }
        }
    
        class RulyCanceler
        {
            object _cancelLocker = new object();
            bool _cancelRequest;
            public bool IsCancellationRequested
            {
                get
                {
                    lock(_cancelLocker)
                    {
                        return _cancelRequest;
                    }
                }
            }
    
            public void Cancel()
            {
                lock(_cancelLocker)
                {
                    _cancelRequest = true;
                }
            }
    
            public void ThrowIfCancellationRequested()
            {
                if(IsCancellationRequested)
                {
                    throw new OperationCanceledException();
                }
            }
        }
    }
  • 上述寫法是安全的cancellation pattern, 但是Work method本身不需要RulyCanceler物件, 因此NET Framework提供Cancellation Token, 讓設置

Cancellation Tokens

  • Net Framework 4.0提供cooperative cancellation pattern的CancellationTokenSource和CancellationToken, 使用方式為:
  1. CancellationTokenSource提供Cancel方法

  2. CancellationToken有IsCancellationRequested屬性和ThrowIfCancellationRequested方法

  • 這個類別是更前面範例更複雜, 拆出2個類別作分開的功能(Cancel和檢查flag)

  • 使用CancellationTokenSource範例如下:

    using System;
    using System.Threading;
    
    namespace CancellationTokenCustom
    {
        class Program
        {
            static void Main(string[] args)
            {
                var cancelSource = new CancellationTokenSource();
                new Thread(() => {
                    try
                    {
                        Work(cancelSource.Token);
                    }
                    catch (OperationCanceledException)
                    {
                        Console.WriteLine("Canceled");
                    }
                }).Start();
    
                Thread.Sleep(1000);
                cancelSource.Cancel();
                Console.ReadLine();
            }
    
            private static void Work(CancellationToken cancelToken)
            {
                while (true)
                {
                    cancelToken.ThrowIfCancellationRequested();
                    try
                    {
                        // other method
                        OtherMethod(cancelToken);
                    }
                    finally
                    {
                        // any required cleanup
                    }
                }
            }
    
            private static void OtherMethod(CancellationToken cancelToken)
            {
                // do stuff...
                for (int i = 0; i < 1000000; ++i)
                {
                }
                Console.WriteLine("I am doing work");
    
    
                cancelToken.ThrowIfCancellationRequested();
            }
    
        }
    }
  • 主要流程為
  1. 先建立CancellationTokenSource物件

  2. 將CancellationTokenSource的CancellationToken代入可支援取消的函式

  3. 在支援取消的函式, 不斷用CancellationToken物件檢查IsCancellationRequested或者透過ThrowIfCancellationRequested來中止程式

  4. 對CancellationTokenSource物件呼叫Cancel方法

  • CancellationToken是struct, 意味這如果有隱式copy給其他的token, 則都是參考同一個CancellationTokenSource

  • CancellationToken的WaitHandle屬性會回傳取消的訊號, 而Register方法可以註冊一個委託事件, 當cancel被呼叫時可以觸發該委託.

  • Cancellation tokens在Net Framework常用的類別如下:

  1. ManualResetEventSlim and SemaphoreSlim

  2. CountdownEvent

  3. Barrier

  4. BlockingCollection

  5. PLINQ and Task Parallel Library

  • 這些類別通常有Wait的函式, 如果有呼叫Wait後再用CancellationToken做cancel, 將取消那Wait的功能. 比起Interrupt更清楚、安全.

Lazy Initialization

  • 類別的Field, 有些在Construct需要花費較多的資源, 比如下方的程式:
    class Foo
    {
    	public readonly Expensive Expensive = new Expensive();
    }
    
    class Expensive 
    {
    	// suppose this is expensive to construct
    }
  • 可以改成一開始是null, 直到存取時才做初始化, 也就是lazily initialize, 比如下方程式:
    class Foo
    {
    	Expensive _expensive;
    	public Expensive Expensive
    	{
    		get
    		{
    			if(_expensive == null)
    			{
    				_expensive = new Expensive();
    			}
    
    			return _expensive;
    		}
    	}
    }
    
    class Expensive 
    {
    	// suppose this is expensive to construct
    }
  • 但是在多執行緒的狀況下, 取Expensive property可能會有重複做new Expensive()的機率, 並非thread-safe. 要達到Thread-safe, 可以加上lock:
    class Foo
    {
    	Expensive _expensive;
    	readonly object _expensiveLock = new object();
    	public Expensive Expensive
    	{
    		get
    		{
    			lock(_expensiveLock)
    			{
    				if(_expensive == null)
    				{
    					_expensive = new Expensive();
    				}
    	
    				return _expensive;
    			}
    		}
    	}
    }
    
    class Expensive 
    {
    	// suppose this is expensive to construct
    }

Lazy

  • .NET Framework 4.0提供Lazy的類別, 能做lazy initialization的功能. Constructor有1個參數isThreadSafe, 設為true時, 代表能支援thread-safe, 若為false, 只能用在single-thread的情境.

  • Lazy在支援thread-safe的實作, 採用Double-checked locking, 更有效率檢查初始化

  • 改成用Lazy且是factory的寫法:

    class Foo
    {
    	Lazy<Expensive> _expensive = new Lazy<Expensive>(() => new Expensive(), true);
    	readonly object _expensiveLock = new object();
    	public Expensive Expensive
    	{
    		get
    		{
    			return _expensive.Value;
    		}
    	}
    }

LazyInitializer

  • LazyInitializer是static類別, 和Lazy差異在
  1. 它的static method可以直接對想做lazy initialization的field, 可以效能優化

  2. 有提供其他的初始化模式, 會有多個執行緒競爭

  • 以下是LazyInitializer使用EnsureInitialized的初始化field的範例:
    class Foo
    {
    	Expensive _expensive;
    	public Expensive Expensive
    	{
    		get
    		{
    			LazyInitializer.EnsureInitialized(ref _expensive, () => new Expensive());
    			return _expensive;
    		}
    	}
    }
  • 可以傳另一個參數做thread race的初始化, 最終只會有1個thread取得1個物件. 這種作法好處是比起Double-checked locking還要快, 因為它不需要lock.

  • 但thread race的初始化很少會用到, 且它有一些缺點:

  1. 如果有多個thread競爭, 數量比CPU core還多, 會比較慢

  2. 潛在得浪費CPU資源做重複的初始化

  3. 初始化的邏輯必須是thread-safe, 比如前述Expensive的Constructor, 有static變數要寫入的話, 就可能是thread-unsafe

  4. initializer對物件的初始化需要dispose時, 而沒有額外的邏輯就無法對浪費的物件做dispose

  • double-checked locking的參考寫法:
    volatile Expensive _expensive;
    public Expensive Expensive
    {
      get
      {
        if (_expensive == null)             // First check (outside lock)
          lock (_expenseLock)
            if (_expensive == null)         // Second check (inside lock)
              _expensive = new Expensive();
        return _expensive;
      }
    }
  • race-to-initialize的參考寫法:
    volatile Expensive _expensive;
    public Expensive Expensive
    {
      get
      {
        if (_expensive == null)
        {
          var instance = new Expensive();
          Interlocked.CompareExchange (ref _expensive, instance, null);
        }
        return _expensive;
      }
    }

Thread-Local Storage

  • Thread擁有自己獨立的資料, 別的Thread無法存取

  • 有3種thread-local storage的實作

[ThreadStatic]

  • 對1個static的field加上ThreadStatic屬性, 因此每個thread存取該變數都是獨立的

  • 缺點是不能用在instance的變數, 且它只有在第1個thread存取它時才初始化值一次, 因此其他thread一開始都拿到預設值.

  • 以下範例是另外建2個thread對ThreadStatic變數_x各自修改值並輸出. Static constructor在程式剛啟動以Main Thread執行, 因此初始化的值5只有給Main Thread, 而t1和t2的_x值是0. 在Sleep 2秒後, Main thread的_x值仍是 5 .

    using System;
    using System.Threading;
    namespace ThreadStaticTest
    {
        class Program
        {
            [ThreadStatic] static int _x;
            static Program()
            {
                _x = 5;
            }
            static void Main(string[] args)
            {
                Thread t1 = new Thread(() => {
                    Console.WriteLine("t1 before: " + _x);
                    _x = 666;
                    Console.WriteLine("t1 after: " + _x);
                });
    
                Thread t2 = new Thread(() => {
                    Console.WriteLine("t2 before: " + _x);
                    _x = 777;
                    Console.WriteLine("t2 after: " + _x);
                });
    
                t1.Start();
                t2.Start();
                Thread.Sleep(2000);
                Console.WriteLine(_x);
                Console.ReadLine();
            }
        }
    }

ThreadLocal

  • 在Net Framework 4.0推出, 能對static 和 instance的field指定預設值

  • 用ThreadLocal建立的值, 要存取時使用它的Value property

  • ThreadLocal有使用Lazy存取, 因此每個Thread再存取時會做Lazy的計算

  • 如下面範例, 每個Thread的_x初始值都是3

    using System;
    using System.Threading;
    
    namespace ThreadLocalTest
    {
        class Program
        {
            static ThreadLocal<int> _x = new ThreadLocal<int> (() => 3);
            static void Main(string[] args)
            {
                Console.WriteLine("Hello World!");
    
                Thread t1 = new Thread(() => {
                    Console.WriteLine("t1 before: " + _x);
                    _x.Value = 666;
                    Console.WriteLine("t1 after: " + _x);
                });
    
                Thread t2 = new Thread(() => {
                    Console.WriteLine("t2 before: " + _x);
                    _x.Value = 777;
                    Console.WriteLine("t2 after: " + _x);
                });
    
                t1.Start();
                t2.Start();
                Thread.Sleep(2000);
                Console.WriteLine(_x);
                Console.ReadLine();
            }
        }
    }
  • 如果是建立instance field, 用Random作為範例, Random類別是thread-unsafe, 因此在multi-thread的環境使用lock之外, 可以用ThreadLocal建立屬於各thread的獨立物件, 如下範例:

    var localRandom = new ThreadLocal(() => new Random());
    Console.WriteLine (localRandom.Value.Next());

  • 前面Random本身有小缺陷, 如果multi-thread在相差10ms之間都對Random取值, 可能會取到相同的值, 因此可以改良帶入一些隨機參數做初始化:

    var localRandom = new ThreadLocal<Random>
     ( () => new Random (Guid.NewGuid().GetHashCode()) );

GetData and SetData

  • 把資料存在LocalDataStoreSlot, 而這slot可以設定名稱或者不命名

  • 由Thread的GetNamedDataSlot方法設定有名稱的slot, 而AllocateDataSlot方法取得不命名的slot

  • Thread的FreeNamedDataSlot方法會釋放有特定名稱的slot與所有thread的關聯, 但原本的slot物件仍可以存取該資料

  • 以下範例是建立名稱為Name的slot和不命名的slot, 分別是存字串MyName和整數值MyNum. Main Thread和另外建立的t1 t2 thread, 對MyName與MyNum都是獨立的值. 最後在呼叫FreeNamedDataSlot之前, 從Name取slot的值仍是"Main name", 但呼叫FreeNamedDataSlot後, 從Name取slot的值變成null.

    using System;
    using System.Threading;
    
    namespace TestLocalDataStoreSlot
    {
        class Program
        {
            static LocalDataStoreSlot _nameSlot = Thread.GetNamedDataSlot("Name");
            static LocalDataStoreSlot _numSlot = Thread.AllocateDataSlot();
    
            static string MyName
            {
                get
                {
                    object data = Thread.GetData(_nameSlot);
                    return data == null ? string.Empty : (string)data;
                }
    
                set
                {
                    Thread.SetData(_nameSlot, value);
                }
            }
    
            static int MyNum
            {
                get
                {
                    object data = Thread.GetData(_numSlot);
                    return data == null ? -1 : (int)data;
                }
    
                set
                {
                    Thread.SetData(_numSlot, value);
                }
            }
    
            static void Main(string[] args)
            {
                Thread t1 = new Thread(() =>
                {
                    Console.WriteLine("t1 before name: " + MyName);
                    MyName = "T1!";
                    Console.WriteLine("t1 after name: " + MyName);
    
                    Console.WriteLine("t1 before num: " + MyNum);
                    MyNum = 555;
                    Console.WriteLine("t1 after num: " + MyNum);
                });
    
                Thread t2 = new Thread(() =>
                {
                    Console.WriteLine("t2 before name: " + MyName);
                    MyName = "T2?";
                    Console.WriteLine("t2 after name: " + MyName);
    
                    Console.WriteLine("t2 before num: " + MyNum);
                    MyNum = 777;
                    Console.WriteLine("t2 after num: " + MyNum);
                });
    
                t1.Start();
                t2.Start();
    
                Console.WriteLine("Main before name: " + MyName);
                MyName = "Main name";
                Console.WriteLine("Main after name: " + MyName);
    
    
    
                Console.WriteLine("Main before num: " + MyNum);
                MyNum = 12345678;
                Console.WriteLine("Main after num: " + MyNum);
    
                Console.ReadLine();
    
                string s1 = Thread.GetData(Thread.GetNamedDataSlot("Name")) as string;
                Console.WriteLine("Main before clear: " + s1);
    
                Thread.FreeNamedDataSlot("Name");
    
                string s2 = Thread.GetData(Thread.GetNamedDataSlot("Name")) as string;
                Console.WriteLine("Main after clear: " + s2);
    
                Console.ReadLine();
            }
        }
    }

Timers

  • Timer可提供某些工作做週期性的執行

  • 在不用Timer的寫法如下, 缺點是會綁住Thread的資源, 且DoSomeAction的任務將逐漸延遲執行

    new Thread (delegate() {
                             while (enabled)
                             {
                               DoSomeAction();
                               Thread.Sleep (TimeSpan.FromHours (24));
                             }
                           }).Start();
  • Net提供4種Timer, 其中2種是一般性的multi-thread timer:
  1. System.Threading.Timer

  2. System.Timers.Timer

  • Single-thread timer:
  1. System.Windows.Forms.Timer (Windows Form timer)

  2. System.Windows.Threading.DispatcherTimer (WPF timer)

  • multi-thread timer能更精準、彈性, 而single-thread timer是安全、方便的執行簡單任務, 比如更新Wiform / WPF 的元件

Multithreaded Timers

  • System.Threading.Timer是最簡單的multi-thread timer

  • 可以呼叫Change方法來改變執行的時間

  • 以下範例是建立Timer, 等5秒後才開始做任務, 每個任務間隔1秒.

    using System;
    using System.Threading;
    
    namespace ThreadingTImer
    {
        class Program
        {
            static void Main(string[] args)
            {
                Timer tmr = new Timer(Tick, "tick...", 5000, 1000);
                Console.ReadLine();
                tmr.Dispose();
            }
    
            static void Tick(object data)
            {
                Console.WriteLine(data);
            }
        }
    }
  • 另一個System.Timers的Timer, 是基於System.Threading.Timer的包裝, 主要增加的功能有:
  1. 實作Component, 可用在Visual Studio’s designer

  2. 不使用Change, 改成Interval property

  3. 不使用直接的委託, 而是Elapsedevent

  4. 用Enabled來啟用或停止timer

  5. 如果對Enabled感到疑惑, 改用Start和Stop方法

  6. AutoReset代表著重複執行的事件

  7. SynchronizingObject property可以呼叫Invoke和BeginInvoke方法, 可以安全呼叫WPF / Winform的元件

  • 以下是System.Timers.Timer的範例, 每0.5秒執行任務, 透過Start和Stop啟用和停止timer.
    using System;
    using System.Timers;
    
    namespace TimersTimer
    {
        class Program
        {
            static void Main(string[] args)
            {
                Timer tmr = new Timer();
                tmr.Interval = 500;
                tmr.Elapsed += tmr_Elapsed;
                tmr.Start();
                Console.ReadLine();
                tmr.Stop();
                Console.ReadLine();
                tmr.Start();
                Console.ReadLine();
                tmr.Dispose();
            }
    
            private static void tmr_Elapsed(object sender, ElapsedEventArgs e)
            {
                Console.WriteLine("Tick");
            }
        }
    }
  • Multi-thread timer是從thread pool的少量thread來支援timer, 也代表每次執行的委託任務, 都可能由不同的thread來執行.

  • Elapsed事件幾乎是很準時的執行, 不管前一段時間的任務執行完畢與否, 因此委託給它的任務必須是thread-safe

  • Multi-thread timer的精準度是基於作業系統 誤差約10~20ms, 如果要更精準, 需要使用native interop來呼叫Windows multimedia timer, 誤差可降至1ms. 這interop定義在winmm.dll. 使用winmm.dll的一般流程:

  1. 呼叫timeBeginPeriod, 通知作業系統需要高精度的timing

  2. 呼叫timeSetEvent啟用timer

  3. 任務完成後, 呼叫timeKillEvent停止timer

  4. 呼叫timeEndPeriod, 通知作業系統不再需要高精度的timing

  • 搜尋 [dllimport winmm.dll timesetevent] 能找到winmm.dll的範例

Single-Threaded Timers

  • Single-thread timer是用來在WPF或Winform, 如果拿到別的應用程式, 則那個timer將不會觸發

  • Winform / WPF的timer並不是基於thread pool, 而是用User interface model的message pumping技術. 也就是Timer觸發的任務都會是同一個thread, 而那thread是一開始建立timer的thread.

  • 使用single-thread timer的好處:

  1. 忘記thread-safe的問題

  2. Timer執行的任務(Tick), 必須前一個任務完成才會觸發下一個

  3. 不需要呼叫元件的Invoke, 能直接在Tick委託任務執行更新UI元件的功能

  • 因為single-thread的限制, 帶來的缺點是:
  1. 除非Tick任務執行地很快, 否則UI將會沒辦法反應
  • WPF / Winform的timer只適合簡單的任務, 否則需要採用multi-thread timer.

  • Single-thread的timer的精準度和multi-thread timer差不多, 會有幾十ms的差異. 而會因為UI的request或其他timer的事件而造成更不準確.

參考資料

  1. Threading in C#, PART 3: USING THREADS, Joseph Albahari.
  2. C# 8.0 in a Nutshell: The Definitive Reference, Joseph Albahari (Amazon)

圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
O口O
iT邦新手 4 級 ‧ 2022-10-05 21:46:15

您好,最近剛好在學習BackgroundWorker,想請教樓主一下關於你文章中的內容:
DoWork事件函式中傳入的DoWorkEventArgs參數和RunWorkerCompleted事件函式中傳入的參數ProgressChangedEventArgs,將它們的屬性Cancel設定為True時:

RunWorkerCompleted事件函式中的RunWorkerCompletedEventArgs參數,為什麼Cancelled屬性會變成True?

懇請先進撥冗指教末學.

glj8989332 iT邦研究生 4 級 ‧ 2022-10-05 23:01:10 檢舉

可以看NET怎實作的~ 主要是他們是用一個 cancelled 變數來傳遞.
BackgroundWorker.cs

我要留言

立即登入留言