一、基于事件的異步模式
基于事件的異步模式 (event-based asynchronous pattern) 提供了簡(jiǎn)單的方式,讓類型提供多線程的能力而不需要顯式啟動(dòng)線程。
- 協(xié)作取消模型。
- 工作線程完成時(shí)安全更新 UI 的能力。
- 轉(zhuǎn)發(fā)異常到完成事件。
EAP 僅是一個(gè)模式,需要開發(fā)人員自己實(shí)現(xiàn)。
EAP 一般會(huì)提供一組成員,在其內(nèi)部管理工作線程,例如 WebClient 類型就使用的 EAP 模式進(jìn)行設(shè)計(jì)。
// 下載數(shù)據(jù)的同步版本。
public byte[] DownloadData (Uri address);
// 下載數(shù)據(jù)的異步版本。
public void DownloadDataAsync (Uri address);
// 下載數(shù)據(jù)的異步版本,支持傳入 token 標(biāo)識(shí)任務(wù)。
public void DownloadDataAsync (Uri address, object userToken);
// 完成時(shí)候的事件,當(dāng)任務(wù)取消,出現(xiàn)異常或者更新 UI 操作都可以才該事件內(nèi)部進(jìn)行操作。
public event DownloadDataCompletedEventHandler DownloadDataCompleted;
public void CancelAsync (object userState); // 取消一個(gè)操作
public bool IsBusy { get; } // 指示是否仍在運(yùn)行
通過 Task 可以很方便的實(shí)現(xiàn) EAP 模式類似的功能。
二、BackgroundWorker
BackgroundWorker 是一個(gè)通用的 EAP 實(shí)現(xiàn),提供了下列功能。
- 協(xié)作取消模型。
- 工作線程完成時(shí)安全更新 UI 的能力。
- 轉(zhuǎn)發(fā)異常到完成事件。
- 報(bào)告工作進(jìn)度的協(xié)議。
BackgroundWorker 使用線程池來創(chuàng)建線程,所以不應(yīng)該在 BackgroundWorker 的線程上調(diào)用 Abort() 方法。
2.1 使用方法
實(shí)例化 BackgroundWorker 對(duì)象,并且掛接 DoWork 事件。
調(diào)用 RunWorkerAsync() 可以傳遞一個(gè) object 參數(shù),以上則是 BackgroundWorker 的最簡(jiǎn)使用方法。
可以為 BackgroundWorker 對(duì)象掛接 RunWorkerCompleted 事件,在該事件內(nèi)部可以對(duì)工作線程執(zhí)行后的異常與結(jié)果進(jìn)行檢查,并且可以直接在該事件內(nèi)部安全地更新 UI 組件。
如果需要支持取消功能,則需要將 WorkerSupportsCancellation 屬性置為 true 。這樣在 DoWork() 事件當(dāng)中就可通過檢查對(duì)象的 CancellationPending 屬性來確定是否被取消,如果是則將 Cancel 置為 true 并結(jié)束工作事件。
調(diào)用 CancelAsync 來請(qǐng)求取消。
開發(fā)人員不一定需要在 CancellationPending 為 true 時(shí)才取消任務(wù),隨時(shí)可以通過將 Cancel 置為 true 來終止任務(wù)。
如果需要添加工作進(jìn)度報(bào)告,則需要將 WorkerReportsProgress 屬性置為 true ,并在 DoWork 事件中周期性地調(diào)用 ReportProcess() 方法來報(bào)告工作進(jìn)度。同時(shí)掛接 ProgressChanged 事件,在其內(nèi)部可以安全地更新 UI 組件,例如設(shè)置進(jìn)度條 Value 值。
下列代碼即是上述功能的完整實(shí)現(xiàn)。
class Program
{
static void Main()
{
var backgroundTest = new BackgroundWorkTest();
backgroundTest.Run();
Console.ReadLine();
}
}
public class BackgroundWorkTest
{
private readonly BackgroundWorker _bw = new BackgroundWorker();
public BackgroundWorkTest()
{
// 綁定工作事件
_bw.DoWork = BwOnDoWork;
// 綁定工作完成事件
_bw.WorkerSupportsCancellation = true;
_bw.RunWorkerCompleted = BwOnRunWorkerCompleted;
// 綁定工作進(jìn)度更新事件
_bw.WorkerReportsProgress = true;
_bw.ProgressChanged = BwOnProgressChanged;
}
private void BwOnProgressChanged(object sender, ProgressChangedEventArgs e)
{
Console.WriteLine($"當(dāng)前進(jìn)度:{e.ProgressPercentage}%");
}
private void BwOnRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
Console.WriteLine("任務(wù)已經(jīng)被取消。");
}
if (e.Error != null)
{
Console.WriteLine("執(zhí)行任務(wù)的過程中出現(xiàn)了異常。");
}
// 在當(dāng)前線程可以直接更新 UI 組件的數(shù)據(jù)
Console.WriteLine($"執(zhí)行完成的結(jié)果:{e.Result}");
}
public void Run()
{
_bw.RunWorkerAsync(10);
}
private void BwOnDoWork(object sender, DoWorkEventArgs e)
{
// 這里是工作線程進(jìn)行執(zhí)行的
Console.WriteLine($"需要計(jì)算的數(shù)據(jù)值為:{e.Argument}");
for (int i = 0; i <= 100; i = 20)
{
if (_bw.CancellationPending)
{
e.Cancel = true;
return;
}
_bw.ReportProgress(i);
}
// 傳遞完成的數(shù)據(jù)給完成事件
e.Result = 1510;
}
}
BackgroundWorker 不是密閉類,用戶可以繼承自 BackgroundWorker 類型,并重寫其 DoWork() 方法以達(dá)到自己的需要。
三、線程的中斷與中止
所有 阻塞 方法在解除阻塞的條件沒有滿足,并且其沒有指定超時(shí)時(shí)間的情況下,會(huì)永久阻塞。
開發(fā)人員可以通過 Thread.Interrupt() 與 Thread.Abort() 方法來解除阻塞。
在使用線程中斷與中止方法的時(shí)候,應(yīng)該十分謹(jǐn)慎,這可能會(huì)導(dǎo)致一些意想不到的情況發(fā)生。
為了演示上面所說的概念,可以編寫如下代碼進(jìn)行測(cè)試。
class Program
{
static void Main()
{
var test = new ThreadInterrupt();
test.Run();
Console.ReadLine();
}
}
public class ThreadInterrupt
{
public void Run()
{
var testThread = new Thread(WorkThread);
testThread.Start();
// 中斷指定的線程
testThread.Interrupt();
}
private void WorkThread()
{
try
{
// 永遠(yuǎn)阻塞
Thread.Sleep(Timeout.Infinite);
}
catch (ThreadInterruptedException e)
{
Console.WriteLine("產(chǎn)生了中斷異常.");
}
Console.WriteLine("線程執(zhí)行完成.");
}
}
3.1 中斷
- 在一個(gè)阻塞線程上調(diào)用
Thread.Interrupt() 方法,會(huì)導(dǎo)致該線程拋出 ThreadInterruptedException 異常,并且強(qiáng)制釋放線程。
- 中斷線程時(shí),除非沒有對(duì)
ThreadInterruptedException 進(jìn)行處理,否則是不會(huì)導(dǎo)致阻塞線程結(jié)束的。
- 隨意中斷一個(gè)線程是十分危險(xiǎn)的,我們可以通過信號(hào)構(gòu)造或者取消構(gòu)造。哪怕是使用
Thread.Abort() 來中止線程,都比中斷線程更加安全。
- 因?yàn)殡S意中斷線程會(huì)導(dǎo)致調(diào)用棧上面的任何框架,或者第三方的方法意外接收到中斷。
3.2 中止
Thread.Abort() 方法在 .NET Core 當(dāng)中無法使用,調(diào)用該方法會(huì)拋出 Thread abort is not supported on this platform. 錯(cuò)誤。
- 在一個(gè)阻塞線程上調(diào)用
Thread.Abort() 方法,效果與中斷相似,但會(huì)拋出一個(gè) ThreadAbortException 異常。
- 該異常在
catch 塊結(jié)束之后會(huì)被重新拋出。
- 未經(jīng)處理的
ThreadAbortException 是僅有的兩個(gè)不會(huì)導(dǎo)致應(yīng)用程序關(guān)閉的異常之一。
- 中止與中斷最大的不同是,中止操作會(huì)立即在執(zhí)行的地方拋出異常。例如中止發(fā)生在
FileStream 的構(gòu)造期間,可能會(huì)導(dǎo)致一個(gè)非托管文件句柄保持打開狀態(tài)導(dǎo)致內(nèi)存泄漏。
四、安全取消
與實(shí)現(xiàn)了 EAP 模式的 BackgroundWorker 類型一樣,我們可以通過協(xié)作模式,使用一個(gè)標(biāo)識(shí)來優(yōu)雅地中止線程。
其核心思路就是封裝一個(gè)取消標(biāo)記,將其傳入到線程當(dāng)中,在線程執(zhí)行時(shí)可以通過這個(gè)取消標(biāo)記來優(yōu)雅中止。
class Program
{
static void Main()
{
var test = new CancelTest();
test.Run();
Console.ReadLine();
}
}
public class CancelToken
{
private readonly object _selfLocker = new object();
private bool _cancelRequest = false;
/// <summary>
/// 當(dāng)前操作是否已經(jīng)被取消。
/// </summary>
public bool IsCancellationRequested
{
get
{
lock (_selfLocker)
{
return _cancelRequest;
}
}
}
/// <summary>
/// 取消操作。
/// </summary>
public void Cancel()
{
lock (_selfLocker)
{
_cancelRequest = true;
}
}
/// <summary>
/// 如果操作已經(jīng)被取消,則拋出異常。
/// </summary>
public void ThrowIfCancellationRequested()
{
lock (_selfLocker)
{
if (_cancelRequest)
{
throw new OperationCanceledException("操作被取消.");
}
}
}
}
public class CancelTest
{
public void Run()
{
var cancelToken = new CancelToken();
var workThread = new Thread(() =>
{
try
{
Work(cancelToken);
}
catch (OperationCanceledException e)
{
Console.WriteLine("任務(wù)已經(jīng)被取消。");
}
});
workThread.Start();
Thread.Sleep(1000);
cancelToken.Cancel();
}
private void Work(CancelToken token)
{
// 模擬耗時(shí)操作
while (true)
{
token.ThrowIfCancellationRequested();
try
{
RealWork(token);
}
finally
{
// 清理資源
}
}
}
private void RealWork(CancelToken token)
{
token.ThrowIfCancellationRequested();
Console.WriteLine("我是真的在工作...");
}
}
4.1 取消標(biāo)記
在 .NET 提供了 CancellationTokenSource 和 CancellationToken 來簡(jiǎn)化取消操作。
如果需要使用這兩個(gè)類,則只需要實(shí)例化一個(gè) CancellationTokenSource 對(duì)象,并將其 Token 屬性傳遞給支持取消的方法,在需要取消的使用調(diào)用 Source 的 Cancel() 即可。
// 偽代碼
var cancelSource = new CancellationTokenSource();
// 啟動(dòng)線程
new Thread(() => work(cancelSource.Token)).Start();
// Work 方法的定義
void Work(CancellationToken cancelToken)
{
cancelToken.ThrowIfCancellationRequested();
}
// 需要取消的時(shí)候,調(diào)用 Cancel 方法。
cancelSource.Cancel();
五、延遲初始化
延遲初始化的作用是緩解類型構(gòu)造的開銷,尤其是某個(gè)類型的構(gòu)造開銷很大的時(shí)候可以按需進(jìn)行構(gòu)造。
// 原始代碼
public class Foo
{
public readonly Expensive Expensive = new Expensive();
}
public class Expensive
{
public Expensive()
{
// ... 構(gòu)造開銷極大
}
}
// 按需構(gòu)造
public class LazyFoo
{
private Expensive _expensive;
public Expensive Expensive
{
get
{
if(_expensive == null) _expensive = new Expensive();
}
}
}
// 按需構(gòu)造的線程安全版本
public class SafeLazyFoo
{
private Expensive _expensive;
private readonly object _lazyLocker = new object();
public Expensive Expensive
{
get
{
lock(_lazyLocker)
{
if(_expensive == null)
{
_expensive = new Expensive();
}
}
}
}
}
在 .NET 4.0 之后提供了一個(gè) Lazy<T> 類型,可以免去上面復(fù)雜的代碼編寫,并且也實(shí)現(xiàn)了雙重鎖定模式。
通過在創(chuàng)建 Lazy<T> 實(shí)例時(shí)傳遞不同的 bool 參數(shù)來決定是否創(chuàng)建線程安全的初始化模式,傳遞了 true 則是線程安全的,傳遞了 false 則不是線程安全的。
public class LazyExpensive
{
}
public class LazyTest
{
// 線程安全版本的延遲初始化對(duì)象。
private Lazy<LazyExpensive> _lazyExpensive = new Lazy<LazyExpensive>(()=>new LazyExpensive(),true);
public LazyExpensive LazyExpensive => _lazyExpensive.Value;
}
5.1 LazyInitializer
LazyInitializer 是一個(gè)靜態(tài)類,基本與 Lazy<T> 相似,但是提供了一系列的靜態(tài)方法,在某些極端情況下可以改善性能。
public class LazyFactoryTest
{
private LazyExpensive _lazyExpensive;
// 雙重鎖定模式。
public LazyExpensive LazyExpensive
{
get
{
LazyInitializer.EnsureInitialized(ref _lazyExpensive, () => new LazyExpensive());
return _lazyExpensive;
}
}
}
LazyInitializer 提供了一個(gè)競(jìng)爭(zhēng)初始化的版本,這種在多核處理器(線程數(shù)與核心數(shù)相等)的情況下速度比雙重鎖定技術(shù)要快。
volatile Expensive _expensive;
public Expensive Expensive
{
get
{
if (_expensive == null)
{
var instance = new Expensive();
Interlocked.CompareExchange (ref _expensive, instance, null);
}
return _expensive;
}
}
六、線程局部存儲(chǔ)
某些數(shù)據(jù)不適合作為全局遍歷和局部變量,但是在整個(gè)調(diào)用棧當(dāng)中又需要進(jìn)行共享,是與執(zhí)行路徑緊密相關(guān)的。所以這里來說,應(yīng)該是在代碼的執(zhí)行路徑當(dāng)中是全局的,這里就可以通過線程來達(dá)到數(shù)據(jù)隔離的效果。例如線程 A 調(diào)用鏈?zhǔn)沁@樣的 A() -> B() -> C()。
對(duì)靜態(tài)字段增加 [ThreadStatic] ,這樣每個(gè)線程就會(huì)擁有獨(dú)立的副本,但僅適用于靜態(tài)字段。
[ThreadStatic] static int _x;
.NET 提供了一個(gè) ThreadLocal<T> 類型可以用于靜態(tài)字段和實(shí)例字段的線程局部存儲(chǔ)。
// 靜態(tài)字段存儲(chǔ)
static ThreadLocal<int> _x = new ThreadLocal<int>(() => 3);
// 實(shí)例字段存儲(chǔ)
var localRandom = new ThreadLocal<Random>(() => new Random());
ThreadLocal<T> 的值是 延遲初始化 的,第一次被使用的時(shí)候 才通過工廠進(jìn)行初始化。
我們可以使用 Thread 提供的 Thread.GetData() 與 Thread.SetData() 方法來將數(shù)據(jù)存儲(chǔ)在線程數(shù)據(jù)槽當(dāng)中。
同一個(gè)數(shù)據(jù)槽可以跨線程使用,而且它在不同的線程當(dāng)中數(shù)據(jù)仍然是獨(dú)立的。
通過 LocalDataStoreSolt 可以構(gòu)建一個(gè)數(shù)據(jù)槽,通過 Thread.GetNamedDataSlot("securityLevel") 來獲得一個(gè)命名槽,可以通過 Thread.FreeNameDataSlot("securityLevel") 來釋放。
如果不需要命名槽,也可以通過 Thread.AllocateDataSlot() 來獲得一個(gè)匿名槽。
class Program
{
static void Main()
{
var test = new ThreadSlotTest();
test.Run();
Console.ReadLine();
}
}
public class ThreadSlotTest
{
// 創(chuàng)建一個(gè)命名槽。
private LocalDataStoreSlot _localDataStoreSlot = Thread.GetNamedDataSlot("命名槽");
// 創(chuàng)建一個(gè)匿名槽。
private LocalDataStoreSlot _anonymousDataStoreSlot = Thread.AllocateDataSlot();
public void Run()
{
new Thread(NamedThreadWork).Start();
new Thread(NamedThreadWork).Start();
new Thread(AnonymousThreadWork).Start();
new Thread(AnonymousThreadWork).Start();
// 釋放命名槽。
Thread.FreeNamedDataSlot("命名槽");
}
// 命名槽測(cè)試。
private void NamedThreadWork()
{
// 設(shè)置命名槽數(shù)據(jù)
Thread.SetData(_localDataStoreSlot,DateTime.UtcNow.Ticks);
var data = Thread.GetData(_localDataStoreSlot);
Console.WriteLine($"命名槽數(shù)據(jù):{data}");
ContinueNamedThreadWork();
}
private void ContinueNamedThreadWork()
{
Console.WriteLine($"延續(xù)方法中命名槽的數(shù)據(jù):{Thread.GetData(_localDataStoreSlot)}");
}
// 匿名槽測(cè)試。
private void AnonymousThreadWork()
{
// 設(shè)置匿名槽數(shù)據(jù)
Thread.SetData(_anonymousDataStoreSlot,DateTime.UtcNow.Ticks);
var data = Thread.GetData(_anonymousDataStoreSlot);
Console.WriteLine($"匿名槽數(shù)據(jù):{data}");
ContinueAnonymousThreadWork();
}
private void ContinueAnonymousThreadWork()
{
Console.WriteLine($"延續(xù)方法中匿名槽的數(shù)據(jù):{Thread.GetData(_anonymousDataStoreSlot)}");
}
}
七、定時(shí)器
7.1 多線程定時(shí)器
- 多線程定時(shí)器使用線程池觸發(fā)時(shí)間,也就意味著
Elapsed 事件可能會(huì)在不同線程當(dāng)中觸發(fā)。
System.Threading.Timer 是最簡(jiǎn)單的多線程定時(shí)器,而 System.Timers.Timer 則是對(duì)于該計(jì)時(shí)器的封裝。
- 多線程定時(shí)器的精度大概在
10 ~ 20 ms。
7.2 單線程定時(shí)器
- 單線程定時(shí)器依賴于 UI 模型的底層消息循環(huán)機(jī)制,所以其
Tick 事件總是在創(chuàng)建該定時(shí)器的線程觸發(fā)。
- 單線程定時(shí)器關(guān)聯(lián)的事件可以安全地操作 UI 組件。
- 精度比多線程定時(shí)器更低,而且更容易使 UI 失去響應(yīng)。
來源:http://www./content-1-114701.html
|