標籤:winform io 使用 ar for 資料 sp on 代碼
71, 區分非同步和多線程的應用情境。
電腦的很多硬體,如硬碟,光碟機,音效卡,網卡都有DMA(Direct Memory Access)功能,它可以不佔用cpu的資源,而非同步提出恰恰就是基於這個的。而多線程是作業系統上的並存執行的代碼,是會佔用cpu資源的。所以關於這兩種的使用情境建議是:1)對於I/0密集型操作使用非同步。2)對於計算密集型操作使用多線程。 72, 線上程同步中使用訊號量。
實值型別是不能被鎖定的,參考型別上的等待機制,分為鎖定和訊號同步。鎖定通過lock關鍵字或Monitor來完成;訊號同步用AutoResetEvent,ManualResetEvent,Semaphore,Mutex,其中Mutex可以在不同進程間同步。 73, 避免鎖定不恰當的對象。
同步對象在需要同步的多個線程中是可見的同一個對象,不要鎖定字串,類型本身,執行個體本身。降低同步對象的可見度,盡量讓它變為private的。 74, 警惕線程的IsBackground。
用Thread類建立的線程是前台線程,如果前台線程不全部退出,應用程式就關不掉。而ThreaadPool裡的線程預設是背景。所以一些關鍵性的操作才需要前台線程,否則應將IsBackground設為true。 75, 警惕線程不會立即啟動。
這取決於作業系統的調度,我們不能假設它會立即啟動。 76, 警惕線程的優先順序。
Thread和ThreadPool建立的線程優先順序都是Nomal的,我們一般沒有必要去修改這個優先順序,對於Window作業系統,優先順序越高在調度時會得到優先執行。 77, 正確停止線程。
1)正如線程不能立即啟動一樣,線程的停止也不是想停就能停,他必須做完必要的工作後才能停止,以傳統的Thread.Abort方法為例,調用這個方法後,如果代碼在和Unmanaged 程式碼打交道,只有等到這部分代碼結束後才能引發ThreadAbortException,即便是在CLR環境中ThreadAbortException也不會立即引發。2)要正確停止線程,不在於調用者採取的行為(Abort),而更多的依賴於線程能否主動響應調用者的停止請求。線程應該開放一個Cancled介面,背景工作執行緒以某種頻率檢測Cancled標識,如果發生改變,線程自己負責退出。.net提供了一個CancellationTokenSource用於協作取消。cts.Token.IsCancellationRequested判斷是否有取消操作,cts.Cancel()發送取消請求,線程結束時的如需回調,可以用cts.Token.Register(() => {// 回調 })註冊一個回呼函數; 78, 應避免線程數量過多。
對於Socket類型的操作應該用非同步,而不要開線程。 79, 使用ThreadPool或BackgroundWorker替代Thread。
ThreadPool線程池可以重複利用閒置線程。BackgroundWorker可以支援進度,完成通知,取消功能,內部也是用的線程池。 80, 用Task來替代ThreadPool。
Task線上程池的基礎上進行了最佳化,Task提供了ContinueWith方法來提供後續任務的執行。Task提供了IsCanceled,IsCompleted,IsFaulted這幾個屬性用於查詢任務的執行結果。並且還支援取消功能。Task的Result屬性用於查詢函數的返回結果,如果執行過程被取消,調用這個屬性會引發AggregateException異常,Task比ThreadPool功能豐富了很多,應盡量多用Task。 81, 使用Parallel簡化同步狀態下的Task使用。
Parallel是一個靜態類,提供了For,Foreach,Invoke方法啟動多個Task執行任務。 82, Parallel簡化但不等同於Task的預設行為。
Parallel的方法調用後是阻塞的,儘管他內部的Task是並行計算的,除非這些Task都執行完了,Parallel後面的代碼才會得到執行。 83, 小心Parallel的陷阱。
Parallel的For方法還提供了線程開始前和結束後執行的方法。但由於Task使用的線程池,比如4次迴圈可能使用了1個或2個線程,這會帶來這兩個方法執行次數的不確定性。另外,toExclusive參數指定值為5的時候,迴圈結束的位置是它的前一個值4,而不是5。 84, 使用PLINQ。
PLINQ即並發執行的LINQ,由於是並發,對於大資料量的查詢,它的效率可能會更好。要使用PLINQ,只用調用資料來源的擴充方法AsParallel方法,在ForEach查詢中,結果就會得到並行計算,當然輸出是無序的。如果要按有序輸出,可以在資料來源上調用AsOrderd方法,如:arr.AsParallel().AsOrdered()。注意在查詢的ForAll方法中,輸出始終是無序的。關於PLINQ的使用,應該進行充分的學習瞭解後再使用,否則可能會帶來意想不到的bug。 85, Task中的異常處理。
調用任務的Wait,WaitAny,WaitAll方法,或查詢Result屬性,如果Task發生了異常,會產生一個AggregateException,我們通過迴圈查詢它的InnerException就可以知道所有的異常。但是Wait等方法是會阻塞主線程的。Task產生的異常,如果在Task內部沒有得到處理,也沒有拋給主線程的話,那麼只有記憶體回收的時候才會觸發TaskScheduler.UnobservedTaskException事件,如果調用了UnobservedTaskException的e.SetObserved();方法,異常就不會再往上傳遞,否則還會進一步觸發CurrentDomain_UnhandledException事件,程式終止。所以,Task內部的異常應該立即得到處理。簡單一點的做法是Task內部邏輯對捕獲的異常進行處理,比如寫日誌之類的。另一種是,在外部定義一個事件,Task內部出現異常時,通過這個事件通知給外部,在這裡進行異常的後續操作,這種方法比較可取。 86, Parallel中的異常處理。
Parallel中的異常和Task中的異常不一樣。它可以拋出給主線程。但是我們也可以在內部聲明一個AggregateExcption,把所有的異常捕獲後,打包到AggregateExcption裡面,一起拋出,個人感覺意義不大。 87, 區分Winform和WPF的執行緒模式。
Winform中是通過ISynchronizeInvoke介面的InvokeRequire屬性來判斷控制項是否處在其他線程中。Winform中的所有控制項都繼承了這個介面。WPF中是通過DispatcherObject提供的CheckAccess和VerifyAccess方法來判斷,其中VerifyAccess判斷不是同一個線程會拋出異常。 88, 並行並不總是速度最快。
因為並行會帶來額外的線程開銷,對於小的迴圈體,計算量不大,並行反而會更慢。 89, 在並行方法中謹慎使用鎖。
並行中有多個線程,加鎖會帶來線程的切換,影響效能。可以適當考慮用Interlock對整型進行原子加減操作而避免線程切換的開銷。如果整個代碼都是需要加鎖的,那麼實際上就沒有並行的意義,用同步的方式或許會更好。
【進階修鍊】——改善C#程式品質(5)