1. 程式人生 > >C#中的多執行緒

C#中的多執行緒

在這一部分,我們討論 Framework 4.0 加入的多執行緒 API,它們可以充分利用多核處理器。

這些 API 可以統稱為 PFX(Parallel Framework,並行框架)。Parallel類與任務並行構造一起被稱為 TPL(Task Parallel Library,任務並行庫)。

Framework 4.0 也增加了一些更底層的執行緒構造,它們針對傳統的多執行緒。我們之前講過的:

在繼續閱讀前,你需要了解第 1 部分 - 第 4 部分中的基本原理,特別是執行緒安全

並行程式設計這一部分提供的所有程式碼都可以在LINQPad中試驗。LINQPad 是一個 C# 程式碼草稿板,可以用來測試程式碼段,而無需建立類、專案或解決方案。想要獲取這些示例程式碼,可以在 LINQPad 左下方的 Samples 標籤頁中點選 Download More Samples

,並且選擇 C# 4.0 in a Nutshell: More Chapters。(譯者注:現在應該是 C# 5.0 in a Nutshell 和 C# 6.0 in a Nutshell 了)

近年來,CPU 時鐘頻率發展陷於停滯,製造商已經將重心轉移至增加核心數量。這對我們程式設計師來說是個問題,因為標準的單執行緒程式碼無法自動利用那些增加的核心來提升程式執行速度。

利用多個核心對大多數服務端應用程式來說很容易,每個執行緒可以獨立處理單獨的客戶端請求,但在桌面環境下就不那麼容易了,因為通常這需要你優化計算密集型程式碼,按如下步驟進行:

  1. 將工作分解成塊。
  2. 多執行緒並行處理這些工作塊。
  3. 以執行緒安全和高效的方式整理結果。

儘管你可以使用傳統的多執行緒構造,但那比較笨拙,尤其是在分解工作和整理結果的步驟。並且,為確保執行緒安全,通常的策略是使用,而它在很多執行緒同時訪問一份資料時會導致大量競爭。

PFX 庫就是專門被設計用來為這些場景提供幫助的。

利用多核心或多處理器的程式設計被稱為並行程式設計(parallel programming)。它是多執行緒這個更寬泛概念的子集。

有兩種分解工作的策略:資料並行(data parallelism)和任務並行(task parallelism)。

當一系列任務需要處理很多資料時,可以讓每個執行緒都執行這一系列(相同的)任務來處理一部分資料(即所有資料的一個子集)。這樣實現的並行化稱為資料並行,因為我們是為執行緒分解了資料。與此相對,任務並行是指對任務進行分解,換句話說就是讓每個執行緒執行不同的任務。

通常,對高度並行的硬體來說,資料並行更簡單,可伸縮性也更好,因為它減少或消除了共享資料(也就減少了競爭和執行緒安全問題)。並且,事實上一般都是資料比任務要多,所以資料並行可以增加併發的可能。

資料並行也有利於結構化並行(structured parallelism),意思是說並行工作單元的啟動和完成是在程式中的同一位置。相對的,任務並行趨向於非結構化,就是說並行工作單元的啟動和完成可能分散在程式各處。結構化並行比較簡單,並且不易出錯,也讓你可以把工作分解和執行緒協調(甚至包括結果整理)這些複雜的任務交給 PFX 庫來完成。

PFX 包含兩層功能。上層是由結構化資料並行 API:PLINQParallel類組成。下層包含任務並行的類,以及一組額外的構造,來幫助你實現並行程式設計。

並行程式設計元件

PLINQ 提供了最豐富的功能:它能夠自動化並行的所有步驟,包括分解工作、多執行緒執行、最後把結果整理成一個輸出序列。它被稱為宣告式(declarative)的,因為你只是宣告希望並行化你的工作(構造一個 LINQ 查詢),然後讓 Framework 來處理實現細節。相對的,另一種方式是指令式(imperative)的,這種方式是需要你顯式編寫程式碼來處理工作分解和結果整理。例如使用Parallel類時,你必須自己整理結果,而如果使用任務並行構造,你還必須自己分解工作。

分解工作 整理結果
PLINQ
PFX 的任務並行 - -

併發集合自旋基元可以幫助你實現低層次的並行程式設計。這很重要,因為 PFX 不僅被設計適用於當今的硬體,也適用於未來更多核心的處理器。如果你希望搬運一堆木塊,並且有 32 個工人,最麻煩的是如何讓工人們搬運木塊時不互相擋道。這與把演算法分解執行在 32 個核心上類似:如果普通的鎖被用於保護公共資源,所產生的阻塞可能意味著同時只有一小部分核心真正在工作。併發集合專門針對於高併發訪問,致力於最小化或消除阻塞。PLINQ 和 Parallel類就依賴於併發集合和自旋基元來實現高效的工作管理。

傳統多執行緒的場景是,即使在單核的機器上,使用多執行緒也有好處,而此時並沒有真正的並行發生。就像我們之前討論過的:保持使用者介面的響應以及同時下載兩個網頁。

這一部分將要講到的一些構造有時對於傳統多執行緒也有用。特別是:

  • PLINQ和 Parallel類在你並行執行操作以及等待它們完成(結構化並行)時有用。這包括非計算密集型任務,例如呼叫 web 服務。
  • 併發集合有時在你需要執行緒安全佇列、棧或字典時有用。

PFX 主要用於並行程式設計:充分利用多核處理器來加速執行計算密集型程式碼。

充分利用多個核心的挑戰在於阿姆達爾定律(Amdahl’s law),它指出通過並行化產生的最大效能提升,取決於有多少必須順序執行的程式碼段。例如,如果一個演算法只有三分之二的執行時間可以並行,即使有無數核心,也無法獲得超過三倍的效能提升。

因此,在使用 PFX 前,有必要先檢查可並行程式碼中的瓶頸。還需要考慮下,你的程式碼是否有必要是計算密集的,優化這裡往往是最簡單有效的方法。然而,這也需要平衡,因為一些優化技術會使程式碼難以並行化。

最容易獲益的是“不好意思不併行的問題(embarrassingly parallel problems)”:工作可以很容易地被分解為多個任務,每個任務自己可以高效執行(結構化並行非常適合這種問題)。例如:很多圖片處理任務、光線跟蹤演算法、數學和密碼學方面的暴力計算和破解。而相反的例子是:實現快速排序演算法的優化版本,想把它實現得好需要一定思考,並且可能需要非結構化並行。

PLINQ 會自動並行化本地的 LINQ 查詢。其優勢在於使用簡單,因為將工作分解和結果整理的負擔交給了 Framework。

使用 PLINQ 時,只要在輸入序列上呼叫AsParallel(),然後像平常一樣繼續 LINQ 查詢就可以了。下邊的查詢計算 3 到 100,000 內的素數,這會充分利用目標機器上的所有核心。

// 使用一個簡單的(未優化)演算法計算素數。
//
// 注意:這一部分提供的所有程式碼都可以在 LINQPad 中試驗。

IEnumerable<int> numbers = Enumerable.Range (3, 100000-3);

var parallelQuery =
  from n in numbers.AsParallel()
  where Enumerable.Range (2, (int) Math.Sqrt (n)).All (i => n % i > 0)
  select n;

int[] primes = parallelQuery.ToArray();

AsParallelSystem.Linq.ParallelEnumerable中的一個擴充套件方法。它使用ParallelQuery<TSource>來封裝輸入,就會將你隨後呼叫的 LINQ 查詢操作符繫結在ParallelEnumerable中定義的另外一組擴充套件方法上。它們提供了所有標準查詢操作符的並行化實現。本質上,它們就是將輸入序列進行分割槽,形成工作塊,並在不同的執行緒上執行,之後再將結果整理成一個輸出序列:

PLINQ 執行

呼叫AsSequential()可以拆封ParallelQuery,使隨後的查詢操作符繫結到標準查詢操作符來順序執行。在呼叫有副作用或非執行緒安全的方法前,有必要這樣做。

對於那些接受兩個輸入序列的查詢操作符(JoinGroupJoinContactUnionIntersectZip)來說,必須在這兩個輸入序列上都使用AsParallel()(否則將丟擲異常)。然而,不需要為中間過程的查詢使用AsParallel,因為 PLINQ 的查詢操作符會輸出另一個ParallelQuery序列。實際上,在這個輸出序列上再次呼叫AsParallel會降低效率,它會強制對序列進行合併和重新分割槽。

mySequence.AsParallel()           // 使用 ParallelQuery<int> 封裝序列
          .Where (n => n > 100)   // 輸出另一個 ParallelQuery<int>
          .AsParallel()           // 不需要,會降低效率!
          .Select (n => n * n)

並非所有的查詢操作符都可以被有效地並行化。對於那些不能的,PLINQ 使用了順序的實現。如果 PLINQ 認為並行化的開銷實際會使查詢變慢,它也會順序執行。

PLINQ 僅適用於本地集合:它無法在 LINQ to SQL 或 Entity Framework 中使用,因為在那些場景中,LINQ 會被翻譯成 SQL 語句,然後在資料庫伺服器上執行。然而,你可以使用 PLINQ 對從資料庫查詢獲得的結果執行進一步的本地查詢。

如果 PLINQ 查詢丟擲異常,它會被封裝進AggregateException重新丟擲,其InnerExceptions屬性包含真正的異常。詳見使用 AggregateException

為什麼 AsParallel 不是預設的?Permalink

我們知道AsParallel可以透明的並行化 LINQ 查詢,那麼問題來了,“微軟為什麼不直接並行化標準查詢操作符,使 PLINQ 成為預設的?”

有很多原因使其成為這種選擇使用(opt-in)的方式。首先,要使 PLINQ 有用,必須要有一定數量的計算密集型任務,它們可以被分配到多個工作執行緒。大多數 LINQ to Objects 的查詢執行非常快,根本不需要並行化,並行化過程中的任務分割槽、結果整理以及執行緒協調反而會使程式變慢。

其次:

  • PLINQ 查詢的輸出(預設情況下)在元素排序方面不同於 LINQ 查詢
  • 如果查詢引用了非執行緒安全的方法,PLINQ 會給出不可靠的結果。

最後,PLINQ 為了進行微調提供了一些鉤子(hook)。把這些累贅加入標準的 LINQ to Objects 的 API 會增加使用障礙。

與普通的 LINQ 查詢一樣,PLINQ 查詢也是延遲估值的。這意味著只有當結果開始被使用時,查詢才會被觸發執行。通常結果是通過一個foreach迴圈被使用(通過轉換操作符也會觸發,例如ToArray,還有返回單個元素或值的操作符)。

當列舉結果時,執行過程與普通的順序查詢略有不同。順序查詢完全由使用方通過“拉”的方式驅動:每個元素都在使用方需要時從輸入序列中被提取。並行查詢通常使用獨立的執行緒從輸入序列中提取元素,這可能比使用方的需要稍微提前了一些(很像一個給播報員使用的提詞機,或者 CD 機中的防震緩衝區)。然後通過查詢鏈並行處理這些元素,將結果儲存在一個小緩衝區中,以準備在需要的時候提供給使用方。如果使用方在列舉過程中暫停或中斷,查詢也會暫停或停止,這樣可以不浪費 CPU 時間或記憶體。

你可以通過在AsParallel之後呼叫WithMergeOptions來調整 PLINQ 的緩衝行為。預設值AutoBuffered通常能產生最佳的整體效果;NotBuffered禁用緩衝,如果你希望儘快看到結果可以使用這個;FullyBuffered在呈現給使用方前快取整個查詢的輸出(OrderByReverse操作符天生以這種方式工作,取元素、聚合和轉換操作符也是一樣)。

並行化查詢操作符的一個副作用是:當整理結果時,不一定能與它們提交時的順序保持一致,就如同之前圖中所示的那樣。換句話說,就是無法像普通的 LINQ 那樣能保證序列的正常順序。

如果你需要保持序列順序,可以通過在AsParallel後呼叫AsOrdered()來強制它保證:

myCollection.AsParallel().AsOrdered()...

在大量元素的情況下呼叫AsOrdered會造成一定效能損失,因為 PLINQ 必須跟蹤每個元素原始位置。

之後你可以通過呼叫AsUnordered來取消AsOrdered的效果:這會引入一個“隨機洗牌點(random shuffle point)”,允許查詢從這個點開始更高效的執行。因此,如果你希望僅為前兩個查詢操作保持輸入序列的順序,可以這樣做:

inputSequence.AsParallel().AsOrdered()
  .QueryOperator1()
  .QueryOperator2()
  .AsUnordered()       // 從這開始順序無關緊要
  .QueryOperator3()
  // ...

AsOrdered不是預設的,因為對於大多數查詢來說,原始的輸入順序無關緊要。換句話說,如果AsOrdered是預設的,你就不得不為大多數並行查詢使用AsUnordered來獲得最好的效能,這會成為負擔。

目前,PLINQ 在能夠並行化的操作上有些實用性限制。這些限制可能會在之後的更新包或 Framework 版本中解決。

下列查詢操作符會阻止查詢的並行化,除非源元素是在它們原始的索引位置:

  • TakeTakeWhileSkipSkipWhile
  • SelectSelectManyElementAt這幾個操作符的帶索引版本

大多數查詢操作符都會改變元素的索引位置(包括可能移除元素的那些操作符,例如Where)。這意味著如果你希望使用上述操作符,就要在查詢開始的地方使用。

下列查詢操作符可以並行化,但會使用代價高昂的分割槽策略,有時可能比順序執行還慢。

  • JoinGroupByGroupJoinDistinctUnionIntersectExcept

Aggregate操作符的帶種子(seed)的過載是不能並行化的,PLINQ 提供了專門的過載來解決。

其它所有操作符都是可以並行化的,然而使用這些操作符並不能確保你的查詢會被並行化。如果 PLINQ 認為進行分割槽的開銷會導致部分查詢變慢,它也許會順序執行查詢。你可以覆蓋這個行為,方法是在AsParallel()之後呼叫如下程式碼來強制並行化:

.WithExecutionMode (ParallelExecutionMode.ForceParallelism)

假設我們希望實現一個拼寫檢查程式,它在處理大文件時,能夠通過充分利用所有可用的核心來快速執行。我們把演算法設計成一個 LINQ 查詢,這樣就可以很容易的並行化它。

第一步是下載英文單詞字典,為了能夠高效查詢,將其放在一個HashSet中:

            
           

相關推薦

C++執行的加鎖機制

問題來源於某面試題: 編寫一個單例模式的類。 #include<iostream> #include<cstdio> #include<cstdlib> using namespace std; class singleStance{

c#執行重新整理UI

    建立後臺執行緒重新整理UI: //建立代理 private delegate void DelegateRefreshUI(); //真正執行重新整理UI的函式 private void freshU

C#執行變數研究

今天在知乎上看到一個問題【為什麼在同一程序中建立不同執行緒,但執行緒各自的變數無法線上程間互相訪問?】。在多執行緒中,每個執行緒都是獨立執行的,不同的執行緒有可能是同一段程式碼,但不會是同一作用域,所以不會共享。而共享記憶體,並沒有作用域之分,同一程序內,不管什麼執行緒都可以通過同一虛擬記憶體地址來訪問,不同

C#在執行使用dictionary時的安全問題

問題出現的情景:在計算一個特徵集中所有特徵與一個數據集的所有例項之間的所有組合距離時,採用多執行緒的方法來提高計算速度。如下,CalculateDistanceThread是計算一個特徵與資料集中所有例項的距離,並將其距離加入到tmp_dist_matrix的字典中。

c++11執行Join函式

寫在前面 Join函式作用: Join thread The function returns when the thread execution has completed.//直到執行緒完成函式才返回 This synchronizes the moment t

c++11執行的互斥量

寫在前面 在多執行緒程式中互斥量的概念十分重要,要保護執行緒之間的共享資料,互斥量的lock、unlock、try_lock函式,以及類模板lock_guard、unique_lock十分重要 栗子 首先先看一下,沒有再共享資料上做任何保護的程式: #include <iost

C# 使用執行訪問winform控制元件

 我們在做winform應用的時候,大部分情況下都會碰到使用多執行緒控制介面上控制元件資訊的問題。然而我們並不能用傳統方法來做這個問題,下面我將詳細的介紹。 首先來看傳統方法: 1 public partial class Form1 : Form 2 { 3

C# Winform專案執行環境下, 如何跨執行對Window窗體控制元件進行安全訪問?

請嘗試執行這段程式碼, 結果你會發現微軟開發工具會提示, Tb_Text.Text = int_Index.ToString(); 涉及"對Windows窗體控制元件進行執行緒安全呼叫", 並給瞭如下的解決方案:https://msdn.microsoft.com/zh-cn/library/ms171728

C# 在執行如何呼叫Winform

問題的產生:  我的WinForm程式中有一個用於更新主視窗的工作執行緒(worker thread),但文件中卻提示我不能在多執行緒中呼叫這個form(為什麼?),而事實上我在呼叫時程式常常會崩掉。請問如何從多執行緒中呼叫form中的方法呢?   解答:  每一個從Con

Linux C語言執行庫Pthread條件變數的的正確用法逐步詳解

(本文的讀者定位是瞭解Pthread常用多執行緒API和Pthread互斥鎖,但是對條件變數完全不知道或者不完全瞭解的人群。如果您對這些都沒什麼概念,可能需要先了解一些基礎知識) Pthread庫的條件變數機制的主要API有三個: int pthread_cond_w

c++11執行的condition_variable(生產者消費者示例)

#include <iostream> #include <string> #include <th

執行執行設定超時退出監控

前言 在寫多執行緒程式時,大多數情況下會先excutor建立執行緒池,然後再建立執行緒,但是對一些讀資料庫或者其他IO操作,容易堵住執行緒,此時就需要給執行緒設定超時時間,幹掉超時的執行緒再重新拉起一個執行緒來,但是java執行緒建立並沒有預留超時引數,研究了一下網上也沒找到

linux下C開發執行程式

轉:https://blog.csdn.net/lingfemg721/article/details/6574804   linux下用C開發多執行緒程式,Linux系統下的多執行緒遵循POSIX執行緒介面,稱為pthread。   #

[轉]c++11 執行 future/promise

[轉自 https://blog.csdn.net/jiange_zh/article/details/51602938] 1. < future >標頭檔案簡介 Classes std::future std::future_error std::packaged_task std::pro

觀察者模式執行執行訂閱事件並順序執行的問題

       對事件釋出訂閱模式中啟動執行緒執行操作,但又要保證執行緒順序執行的一些思考和實踐,在開發過程中,經常會遇到需要使用事件來觸發方法執行的情況,比如CS中按鈕的點選事件,滑鼠移動事件,鍵盤監聽事件等等,有時候需要執行比較耗時的任務,但並不希望阻塞主執

Spring4.x執行使用

直接上程式碼: 一:配置類 import java.util.concurrent.Executor; import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; import org.springfram

C#非同步執行總結(delegate、Thread、Task、ThreadPool、Parallel、async、cancel)

同步與非同步多執行緒的區別: 1、同步方法卡介面(UI執行緒忙於計算);非同步多執行緒不卡介面(主執行緒閒置,子執行緒在計算) 2、同步方法慢(CPU利用率低、資源耗費少);非同步多執行緒快(CPU利用率高、資源耗費多) 3、同步方法是有序的;非同步方法是無序的(啟動無序、執行時間不確定、結

c++11執行 thread

 1.thread建構函式 default (1) thread() noexcept; initialization (2) template <class Fn, class... Args> explicit

java執行一定快嗎?看完就知道!!!

理解上下文切換   即使是單核處理器也支援多執行緒執行程式碼,CPU通過每個執行緒分配CPU時間片來實現這個機制.時間片是CPU分配給多個執行緒的時間,因為時間片非常短,所以CPU通過不停的切換執行緒執行,讓我們感覺多個執行緒是同時執行的,時間片一般是幾十毫秒(ms).  

Python執行總結

Python中的多執行緒 多執行緒 一個程序中有多個執行緒就是多執行緒。 一個程序中至少有一個執行緒,並作為程式的入口,這個就是主執行緒。一個程序至少有一個主程序,其他執行緒稱為工作執行緒。 執行緒安全:執行緒執行一段程式碼,不會產生不確定的結果,那這段程式碼就是執行緒安全。(例如pr