.NET併發編程-任務函數並行

本系列學習在.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(* ̄︶ ̄)

圖片

圖片

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章