本篇同步发文于个人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的事件而造成更不準确.