.NET併發程式設計-任務函式並行

那是山發表於2021-03-14

本系列學習在.NET中的併發並行程式設計模式,實戰技巧

請問普通:

被門夾過的核桃還能補腦嗎

本小節開始學習基於任務的函式式並行。本系列保證最少程式碼呈現量,雖然talk is cheap, show me the code被奉為圭臬,我的學習習慣是,只學習知識點,程式碼不在當下立馬要用的時候不會認真去讀的,更何況在大多時候在手機閱讀更不順暢。

本小節介紹一種簡單的函式組合來並行執行任務方式,達到不阻塞程式提高效能的目的。

1、任務並行2、.NET中的任務並行化支援3、.NET任務並行庫4、C#void問題5、延續傳遞風格CPS6、組合策略

1、任務並行

回顧下什麼是任務並行,任務並行是在相同或不同的資料集上用時執行多個不同的函式,區別於資料並行是在資料集的元素之間同時執行同一個函式。

生產中可能會涉及不同的任務函式,處理不同的複雜的結構資料,通過利用在.NET提供的一些模型工具箱我們可以較為簡便的任務並行跑起來。

2、.NET中的任務並行化支援

由淺入深,.NET1.0開始就提供執行緒的訪問控制。System.Thread,可以程式碼控制建立啟動銷燬現場。但執行緒的建立開銷比較大,後面有提供了ThreadPool類,執行緒池有助於克服效能問題。在初始化期間就載入了一組執行緒,然後重用這些執行緒,避免了頻繁建立銷燬執行緒的開銷。

Action<string> downloadSite = url => {
    var content = new WebClient().DownloadString(url);
    Console.WriteLine($"The size of the web site {url} is {content.Length}");
}; 

var threadA = new Thread(() => downloadSite("http://www.nasdaq.com"));
var threadB = new Thread(() => downloadSite("http://www.bbc.com"));

threadA.Start();
threadB.Start(); 
threadA.Join();
threadB.Join();  

ThreadPool.QueueUserWorkItem(o => downloadSite("http://www.nasdaq.com"));
ThreadPool.QueueUserWorkItem(o => downloadSite("http://www.bbc.com")); 

像上面所示傳統的方式也很繁瑣,而且有很多弊端,比如無法獲取結果,沒有內建通知等。因為又提供了TPL任務並行庫。

3、.NET任務並行庫

TPL在ThreadPool上實現了很多優化,簡化了新增並行的過程,通過Task物件提供支援,以取消和管理狀態,處理和傳播異常,以及控制工程執行緒的執行。

TPL提供很多種排程任務的方式,Invoke是最簡單的一種。類似的還有Parallel.ForEach

System.Threading.Tasks.Parallel.Invoke(
        Action(fun () -> convertImageTo3D (pathCombine "MonaLisa.jpg") (pathCombine "MonaLisa3D.jpg")),
        Action(fun () -> setGrayscale (pathCombine "LadyErmine.jpg") (pathCombine "LadyErmineRed.jpg")),
        Action(fun () -> setRedscale (pathCombine "GinevraBenci.jpg") (pathCombine "GinevraBenciGray.jpg")))

此方法接收任意數量的action委託引數,併為每一個委託建立任務。但是,action委託沒有輸入引數,並且返回void,這樣的函式會有副作用。當所有任務終止時,Invoke方法將控制權交回給主執行緒以繼續執行後續流程。在並行執行獨立的異構任務時,就是針對不同的結構資料,此方法挺有效的。

弊端也很明顯,沒有輸入型別,返回為Void,也就限制了組合使用,執行順序也無法保障。

4、C#void問題

和Null類似,Void也是一個頭疼的問題。函數語言程式設計語言中每一個函式都有返回值,包括與void類似情況的unit型別,但是與void不同的是該值被視為一個值,概念上與bool和int沒多大區別。

unit是缺少其他特定值的表示式的型別,像列印日誌到控制檯,寫入檔案等,沒有特定的內容需要返回,因為函式需要返回unit。unit就是C#的void在F#中的等價產物。

在FP的函式就是一個對映,一個輸入對映一個輸出,這樣函式才是無副作用的。在指令式程式設計語言中丟失了這個概念。

可以參考F#unit自定義個C#中的unit

public struct Unit : IEquatable<Unit> 
{
    public static readonly Unit Default = new Unit();  

    public override int GetHashCode() => 0;        
    public override bool Equals(object obj=> obj is Unit; 

    public override string ToString() => "()";

    public bool Equals(Unit other=> true;    
    public static bool operator ==(Unit lhs, Unit rhs) => true
    public static bool operator !=(Unit lhs, Unit rhs) => false
}

這樣可以讓每個函式都有返回值來確認函式已完成,並且任何使用action委託的地方都可以使用func代替,只需要給func執行返回值為unit即可。return Unit.Default;

5、延續傳遞風格CPS

一種更新更好的機制是將剩餘的計算傳遞給(線上程完成執行後執行的)回撥函式以繼續工作。這種技術在FP中被稱為延續傳遞風格Continuation-Passing Style CPS。通過將當前函式的結果傳遞給下一個函式,以延續的形式為你提供執行控制。

.NET中Task類提供比Thread更高階別的抽象,以便於控制每個每個任務操作的生命週期。

Task monaLisaTask = Task.Factory.StartNew(() => convertImageTo3D("MonaLisa.jpg""MonaLisa3D.jpg"));
Task ladyErineTask = new Task(() => setGrayscale("ladyErine.jpg""ladyErine3D.jpg"));
ladyErineTask.Start();
Task ginevraBenciTask = Task.Run(() => setRedscale("ginevraBenci.jpg""ginevraBenci3D.jpg"));

Task提供三種直接建立任務的方式,new Task方式可以控制在何處Start啟動任務。

通過Task的ContinueWith可以延續任務。FromCurrentSynchronizationContext捕獲當前不同上下文中執行,如果需要同步UI請使用,會自動選擇合適的上下文去更新。

Task ginevraBenciTask = Task.Run<Bitmap>(() => setRedscale("ginevraBenci.jpg""ginevraBenci3D.jpg"));
ginevraBenciTask.ContinueWith(bitmap => {
    var bitmapImage = bitmap.Result; 
}, TaskScheduler.FromCurrentSynchronizationContext());

6、組合策略

使用ContinueWith可以延續任務,但較多的延續,程式碼將比較繁瑣,而且如果要新增錯誤處理或取消支援就不好新增了。所以要使用到函式閉包中說到的函式組合。

C#實現組合Compose函式如下

Func<A,C> Compose<A,B,C>(this Func<A.B> f ,Func<B,C> g)=>(n)=>g(f(n))

在並行Task中,f,g應該是獨立執行的,當做兩個任務,f任務返回Task(B),g任務返回Task(C),所以改造如下

Func<A,Task<C>> Compose<A,B,C>(this Func<A.Task<B>> f ,Func<B,Task<C>> g)=>(n)=>g(f(n))

但是有問題的,f(n)返回型別Task(B),無法直接給函式g使用,輸入型別不一致。

這個使用需要使用FP中常見的一種模式,單子Monad。對於指令式程式設計語言的程式設計師來說,壓根沒聽過啊。其實也是一種設計模式,就像裝飾器和介面卡一樣。單子是一種數學模式,它通過封裝程式邏輯,保持函式式的純粹性以及提供一個強大的組合工具以組合使用提供型別的計算來控制副作用的執行。比較晦澀難懂,還需要多看看官方文件才行。

我們定義一個Bind來提升型別,包裝B,然後組合就像下面這樣了。

static Task<C> Bind<B, C>(Task<B> b, Func<B, Task<C>> g)
{
    return g(b.Result);
}
Func<A,Task<C>> Compose<A,B,C>(this Func<A.Task<B>> f ,Func<B,Task<C>> g)=>(n)=>bind(f(n),g)

請問普通:

被門夾過的核桃還能補腦嗎

to be contiued!
下集:任務非同步模型

    to be contiued!下集:任務非同步模型
上週學了兩天摩托,那個受罪,比上班還累,早8晚8,但都是一群熱愛的孩子們,誰都沒有摸魚。而且大部分是北京本地孩子,學著玩兒。ps邊三輪有沒有要進村的感覺hahahayiha(* ̄︶ ̄)

圖片

圖片

相關文章