本篇同步發文於個人Blog: [讀書筆記] Threading in C# - PART 3: USING THREADS
合作式取消模型
當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的魅力大減
合作的取消模型
當Worker完成, 可以安全更新WPF/Winform的Control
把Exception傳遞到完成事件
有個進度回報的protocol
實作IComponent, 在Design time(Ex: Visual Studio Designer)可以被託管
建立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
設置WorkerSupportsCancellation屬性為true
定期在DoWork的委託事件內檢查CancellationPending這個boolean值, 如果它是true, 則可以設置DoWorkEventArgs的Cancel為true並做return. 如果DoWork的工作太困難而不能繼續執行, 也可以不理會CancellationPending的狀態而直接設Cancel為true
呼叫CancelAsync來請求取消任務
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;
}
}
}
可以繼承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和Abort能停止Blocked的thread
Abort也能停止非block的thread, 比如一直在無限迴圈執行的thread, 所以Abort會在特定場合使用, 但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();
}
}
}
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();
}
}
}
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也是強制釋放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機制是這樣做
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();
}
}
}
}
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
class Foo
{
public readonly Expensive Expensive = new Expensive();
}
class Expensive
{
// suppose this is expensive to construct
}
class Foo
{
Expensive _expensive;
public Expensive Expensive
{
get
{
if(_expensive == null)
{
_expensive = new Expensive();
}
return _expensive;
}
}
}
class Expensive
{
// suppose this is expensive to construct
}
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
}
.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;
}
}
}
它的static method可以直接對想做lazy initialization的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
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;
}
}
volatile Expensive _expensive;
public Expensive Expensive
{
get
{
if (_expensive == null)
{
var instance = new Expensive();
Interlocked.CompareExchange (ref _expensive, instance, null);
}
return _expensive;
}
}
Thread擁有自己獨立的資料, 別的Thread無法存取
有3種thread-local storage的實作
對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();
}
}
}
在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()) );
把資料存在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();
}
}
}
Timer可提供某些工作做週期性的執行
在不用Timer的寫法如下, 缺點是會綁住Thread的資源, 且DoSomeAction的任務將逐漸延遲執行
new Thread (delegate() {
while (enabled)
{
DoSomeAction();
Thread.Sleep (TimeSpan.FromHours (24));
}
}).Start();
System.Threading.Timer
System.Timers.Timer
System.Windows.Forms.Timer (Windows Form timer)
System.Windows.Threading.DispatcherTimer (WPF timer)
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);
}
}
}
實作Component, 可用在Visual Studio’s designer
不使用Change, 改成Interval property
不使用直接的委託, 而是Elapsedevent
用Enabled來啟用或停止timer
如果對Enabled感到疑惑, 改用Start和Stop方法
AutoReset代表著重複執行的事件
SynchronizingObject property可以呼叫Invoke和BeginInvoke方法, 可以安全呼叫WPF / Winform的元件
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
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元件的功能
WPF / Winform的timer只適合簡單的任務, 否則需要採用multi-thread timer.
Single-thread的timer的精準度和multi-thread timer差不多, 會有幾十ms的差異. 而會因為UI的request或其他timer的事件而造成更不準確.
您好,最近剛好在學習BackgroundWorker,想請教樓主一下關於你文章中的內容:
DoWork事件函式中傳入的DoWorkEventArgs參數和RunWorkerCompleted事件函式中傳入的參數ProgressChangedEventArgs,將它們的屬性Cancel設定為True時:
RunWorkerCompleted事件函式中的RunWorkerCompletedEventArgs參數,為什麼Cancelled屬性會變成True?
懇請先進撥冗指教末學.
可以看NET怎實作的~ 主要是他們是用一個 cancelled
變數來傳遞.
BackgroundWorker.cs