[读书笔记] Threading in C# - PART 3: USING THREADS

本篇同步发文於个人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)

<<:  [读书笔记] Threading in C# - PART 2: BASIC SYNCHRONIZATION

>>:  Day 9 - 目前(传统)的机器学习三步骤(4)-训练之测试

Day22 - 【概念篇】Keycloak使用基本概念 - 前导

本系列文之後也会置於个人网站 在这之前,都是先请大家照着做,没有好好说明关於Keycloak的使用...

# Day3 Virtual Memory Layout on RISC-V Linux

星期日,感觉是个适合算数的好日子,所以今天的文件,就决定是你了 Virtual Memory Lay...

Android学习笔记04

kotlin+mvvm+databinding+recyclerview 上一篇讲了一般kotlin...

TypeScript 能手养成之旅 Day 10 物件型别-扩充型别-列举(Enum)

前言 上集我们介绍到 Enum 基础用法,今天将来讲解其它用法。 字串列举(String enum)...

[13th-铁人赛]Day 2:Modern CSS 超详细新手攻略 - 入门

WHO? WHAT? CSS到底是谁? CSS的全名为Cascading Style Sheets,...