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

本篇同步发文于个人Blog: [读书笔记] Threading in C# - PART 3: USING THREADS

The Event-Based Asynchronous Pattern

EAP可启用多执行绪且不需要Consumer主动或管理thread, 有以下特徵

合作式取消模型

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

在完成的事件传递Exception

EAP只是个Pattern, 实作上最常见的有BackgroundWorker, WebClient等

这些Class包含有*Async的方法, 通常就是EAP. 呼叫*Async的方法, 将任务交给其他thread执行, 而任务完成后, 会触发Completed事件

*Completed事件的参数包含这些:

有个flag标示该任务是否有被取消

有exception抛出时, 包装在Error物件

call function代入的userToken

使用EAP的设计, 如果有遵循APM的规则, 可以节省Thread

之后的Task实作和EAP很相似, 让EAP的魅力大减

BackgroundWorker

BackgroundWorker是在System.ComponentModel, 符合EAP设计, 并有以下特徵:

合作的取消模型

当Worker完成, 可以安全更新WPF/Winform的Control

把Exception传递到完成事件

有个进度回报的protocol

实作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, 要以下步骤:

设置WorkerReportsProgress属性为true

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

新增ProgressChanged事件处理, 查询前面代入的进度值, 用ProgressPercentage参数查

在ProgressChanged也能更新UI Control

如果要增加Cancellation的功能:

设置WorkerSupportsCancellation属性为true

定期在DoWork的委託事件内检查CancellationPending这个boolean值, 如果它是true, 则可以设置DoWorkEventArgs的Cancel为true并做return. 如果DoWork的工作太困难而不能继续执行, 也可以不理会CancellationPending的状态而直接设Cancel为true

呼叫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:

在abort该thread后, 连它的AppDomain也要中止. 比如Unit testing

对自身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, 使用方式为:

CancellationTokenSource提供Cancel方法

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();            }            }    }
主要流程为

先建立CancellationTokenSource物件

将CancellationTokenSource的CancellationToken代入可支援取消的函式

在支援取消的函式, 不断用CancellationToken物件检查IsCancellationRequested或者透过ThrowIfCancellationRequested来中止程式

对CancellationTokenSource物件呼叫Cancel方法

CancellationToken是struct, 意味这如果有隐式copy给其他的token, 则都是参考同一个CancellationTokenSource

CancellationToken的WaitHandle属性会回传取消的讯号, 而Register方法可以注册一个委託事件, 当cancel被呼叫时可以触发该委託.

Cancellation tokens在Net Framework常用的类别如下:

ManualResetEventSlim and SemaphoreSlim

CountdownEvent

Barrier

BlockingCollection

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差异在

它的static method可以直接对想做lazy initialization的field, 可以效能优化

有提供其他的初始化模式, 会有多个执行绪竞争

以下是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的初始化很少会用到, 且它有一些缺点:

如果有多个thread竞争, 数量比CPU core还多, 会比较慢

潜在得浪费CPU资源做重複的初始化

初始化的逻辑必须是thread-safe, 比如前述Expensive的Constructor, 有static变数要写入的话, 就可能是thread-unsafe

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:

System.Threading.Timer

System.Timers.Timer

Single-thread timer:

System.Windows.Forms.Timer (Windows Form timer)

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的包装, 主要增加的功能有:

实作Component, 可用在Visual Studio’s designer

不使用Change, 改成Interval property

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

用Enabled来启用或停止timer

如果对Enabled感到疑惑, 改用Start和Stop方法

AutoReset代表着重複执行的事件

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的一般流程:

呼叫timeBeginPeriod, 通知作业系统需要高精度的timing

呼叫timeSetEvent启用timer

任务完成后, 呼叫timeKillEvent停止timer

呼叫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的好处:

忘记thread-safe的问题

Timer执行的任务(Tick), 必须前一个任务完成才会触发下一个

不需要呼叫元件的Invoke, 能直接在Tick委託任务执行更新UI元件的功能

因为single-thread的限制, 带来的缺点是:除非Tick任务执行地很快, 否则UI将会没办法反应

WPF / Winform的timer只适合简单的任务, 否则需要採用multi-thread timer.

Single-thread的timer的精準度和multi-thread timer差不多, 会有几十ms的差异. 而会因为UI的request或其他timer的事件而造成更不準确.

参考资料

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

关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章