1、簡介
為什么MS要推出Task,而不推Thread和ThreadPool,以下是我的見解:
(1)、Thread的Api並不靠譜,甚至MS自己都不推薦,原因,它將整個Thread類都不開放給Windows Sotre程序,且它的Api過於強大,如果在程序中過度使用,維護的成本太高,想想代碼中充斥着掛起線程,阻塞線程、后期的應用程序很難維護.
(2)、ThreadPool最大的問題是,所有的輔助線程都是異步的,沒有向Thread的Join方法那樣去等待一個線程執行完,然后執行回調函數的機制,也就是你無法判斷線程什么時候執行完,也沒有機制獲得線程的返回值,所有MS推出了Task來解決Thread和ThreadPool的問題
當然最主要的是,Thread和Thread好用.因為Task是它們的升級版,升級版當然比較好.
2、Task的缺點
雖然Task以其強大的Api,以及封裝,讓我們在CLR環境下,能完成高效率的編程,但是它並不是沒有缺點的,高效率的背后,肯定帶來的性能的損失,這一點很多類似的框架都能說明,比如EF,強大的背后,大量的使用了反射等操作,所以雖然開發效率提升了,但是性能卻下降了,這里不想說太多,所以簡單的api可能不會產生過多的性能損耗,所以這也是為什么大型互聯網項目,更願意使用原生Ado或者Dapper去做.所以這些在我們的實際開發中,這些都需要我們去權衡.有得必有失.下面來簡單的說下Task具體在哪里會產生性能損失:
很直觀,直接分析ThreadPool類和Task類的構造:
ThreadPool類

很簡潔,沒有任何的字段和屬性!
Task類,1700行代碼,里面有大量的字段和屬性,大致如下:

還包括對父任務的引用、任務調度器(TaskScheduler)的引用、對回調方法的引用、對執行上下文(ExecutionContext)的引用、對ManualResetEventSlim信號量的引用、還有CancellationToken取消信號量(我把它理解為信號量)的引用、一個ContinueWithTask的任務集合的引用、還有未拋出異常的Task對象集合的引用等等,這些后面的文章都會介紹.

所以,不分析具體的性能損耗點,但是單單兩個類的構造,你就能清楚使用那個類創建線程所產生的性能消耗大.
3、實戰
(1)、不帶返回值,實現和ThreadPool線程池線程一樣的效果
static void Main(string[] args)
{
var result=Task.Run(() => Calculate("這個參數很六啊"));
Console.WriteLine("主線程有沒有在繼續執行,look look");
Console.ReadKey();
}
static void Calculate(string param)
{
Console.WriteLine("子線程開始執行,帶着主線程給它傳遞的參數呢!參數是:{0}",param);
Thread.Sleep(2000);
Console.WriteLine("子線程執行完了");
}

根據輸出,發現主線程並沒有等帶子線程執行完畢,通過開啟一個新線程之后,立刻返回去執行它自己的任務.
(2)、帶返回值
static void Main(string[] args)
{
var result=Task.Run(() => Calculate(1));
Console.WriteLine(result.Result);
Console.WriteLine("主線程有沒有在繼續執行,look look");
Console.ReadKey();
}
/// <summary>
/// 簡單遞歸計算n+(n-1)+.....+1
/// </summary>
/// <param name="param"></param>
/// <returns></returns>
static int Calculate(int param)
{
if (param == 1)
return 1;
return param + Calculate(param - 1);
}

無論給Calculate方法傳遞的參數多小,主線程都等待子線程返回結果后,在繼續執行它的任務.所以可以得出結論.調用子線程返回值的Result屬性

相等於調用了Wait方法,當然Task確實提供了這個實例方法,但是使用Result屬性一樣有這個效果.主線程會等待子線程執行完畢在執行它的任務.
(3)、關於Task的小要點
當主線程通過Task開啟了一個子線程之后,返回做自己的事情,當它執行到Wait方法,這個時候主線程會阻塞,CPU的執行速度很快,所以它會去判斷子線程有沒有開始執行,如果沒有執行,那么它會自己去做子線程的任務,而不是開啟一個新的線程去做.這樣就節約了系統資源.這樣就不會存在線程阻塞的情況.所有事情都由主線程干完.
(4)、關於簡單的死鎖問題
一般死鎖的產生,都是多線程爭用相同的資源導致的.下面就來重現一下.
private static object lockObj = new object();
static void Main(string[] args)
{
var result=Task.Run(() => Calculate(100));
lock (lockObj)
{
Console.WriteLine("主線程這個時候爭用了lockObj鎖,並執行子線程");
Console.WriteLine(result.Result);
}
Console.WriteLine("主線程有沒有在繼續執行,look look");
Console.ReadKey();
}
/// <summary>
/// 簡單遞歸計算n+(n-1)+.....+1
/// </summary>
/// <param name="param"></param>
/// <returns></returns>
static int Calculate(int param)
{
lock (lockObj)
{
Console.WriteLine("子線程這個時候也去爭用lockObj鎖,發現主線程已經爭用了這個鎖,那么它等待主線程釋放這個鎖,但是主線程正等待它執行完!");
Console.WriteLine("好了,這個時候就發生了死鎖現象.主線程等子線程執行完,子線程等主線程釋放lockObj鎖,兩個線程在相互等待,死鎖了");
}
if (param == 1)
return 1;
return param + Calculate(param - 1);
}

光標一直在那閃啊閃,好吧,那就都等着吧.誰都執行不下去了.
解決辦法很簡單.在創建一個新的鎖,這個就不代碼演示了.
(5)、取消Task創建的子線程
取消Task創建的線程和取消ThreadPool創建的子線程一樣,通過CancellationTokenSource類實現,代碼如下:
var cancellationSource = new CancellationTokenSource();
cancellationSource.Cancel();
try {
Task.Run(() => ChildThread(cancellationSource.Token));
}
catch(AggregateException ex)
{
//處理子線程拋出的異常
ex.Handle((x) => x is OperationCanceledException);
}
Console.WriteLine("主線程繼續做它的事情");
Console.ReadKey();
}
/// <summary>
/// 子線程
/// </summary>
static void ChildThread(CancellationToken token)
{
token.ThrowIfCancellationRequested();
Console.WriteLine("子線程做完了它的事情");
}


(6)、任務完成時啟動新的任務 ContinueWith
當使用Task進行多線程任務開發時,不建議使用Wait方法或者Result屬性,去阻塞主線程,原因如下:
i、會卡界面
ii、伸縮性好的軟件,不會這么做,除非迫不得已
iii、很有可能創建新的線程,浪費資源(如果主線程執行的足夠快,它可能自己去完成子線程的任務,而不是創建新的線程)
代碼如下:
static void Main(string[] args)
{
//開啟一個子線程進行計算操作
var watch = Stopwatch.StartNew();
Task<int> task=Task.Run(() => ChildThreadOne());
//當子線程一計算完畢之后,開啟一個新的線程去執行輸出子線程一的結果,這里新的線程不會阻塞
//只有當子線程完成計算輸出后,它才會開啟,並輸出子線程的值
//所以該程序並不會發生線程阻塞的情況
task.ContinueWith(x =>
{
watch.Stop();
Console.WriteLine("輸出子線程一的返回值:{0},耗時:{1}", task.Result, watch.ElapsedMilliseconds / 1000);
});
Console.WriteLine("主線程繼續做它的事情");
Console.ReadKey();
}
/// <summary>
/// 子線程一
/// </summary>
/// <returns></returns>
static int ChildThreadOne()
{
Thread.Sleep(2000);//模擬長時間運算
return 666;
}

這里注意兩點:
(1)、這里ContinueWith會檢測到子線程完成之后,立即啟動一個新的線程去顯示結果.不會存在子線程還沒有完成計算的情況下,輸出一個空值,或者發生異常,這一點,CLR能保證.
(2)、這里ContinueWith會返回一個Task對象示例,所以可以調用Wait方法,或者Result屬性,單一般不建議這么做,還是那句話會阻塞線程.一般都忽略這個Task實例,所以需要謹慎使用.
static void Main(string[] args)
{
Task<int> task = Task.Run(() => ChildThreadOne());
var t1=task.ContinueWith((x) => ChildOneContinueOne(task.Result));
var t2=task.ContinueWith((x) => ChildOneContinueTwo(task.Result));
t1.ContinueWith(x => { Console.WriteLine("輸出子線程一的計算結果加10后的結果值:{0}", t1.Result); });
t2.ContinueWith(x => { Console.WriteLine("輸出子線程一的計算結果乘10后的結果值:{0}", t2.Result); });
Console.WriteLine("主線程繼續做它的事情");
Console.ReadKey();
}
/// <summary>
/// 子線程一
/// </summary>
/// <returns></returns>
static int ChildThreadOne()
{
Thread.Sleep(2000);//模擬長時間運算
return 10;
}
/// <summary>
/// 在子線程一完成計算后,開啟一個新的線程對子線程一的結果進行+66操作
/// </summary>
/// <param name="childOneResult"></param>
/// <returns></returns>
static int ChildOneContinueOne(int childOneResult)
{
Console.WriteLine("ChildOneContinueOne線程拿到的子線程一的結果值為{0}", childOneResult);
Thread.Sleep(2000);//模擬長時間計算任務
return 10 + childOneResult;
}
/// <summary>
/// 在子線程一完成計算后,開啟一個新的線程對子線程一的結果進行乘66操作
/// </summary>
/// <param name="childOneResult"></param>
/// <returns></returns>
static int ChildOneContinueTwo(int childOneResult)
{
Console.WriteLine("ChildOneContinueTwo線程拿到的子線程一的結果值為{0}", childOneResult);
Thread.Sleep(2000);//模擬長時間計算任務
return 10 * childOneResult;
}

用ContinueWith做了一件有趣的事情,大致思路是我們在開發過程中會遇到,到我們拿到一個線程的返回值后,立即開啟兩個新的線程去做兩個方向的任務,如下圖:

這在開發中經常使用,整個過程沒有任務阻塞線程.暫時沒有發現多線程爭用問題.
原理淺析:
Task對象實例包含一個ContinueWith任務的一個集合,所以可以使用Task對象多次調用ContinueWith方法(就像上面的代碼一樣),所有的線程都會進入線程池的隊列中,當Task任務執行完畢,線程池回依次調用它們.
(2)、使用ContinueWith中產生的特殊情況
當子線程發生異常、取消、或者超時時,這個時候就要告訴線程池如何處理喚起線程,而不是無視,子線程的異常,所以MS給ContinueWith提供了一個TaskContinuationOptions枚舉,來處理這個問題.下面介紹幾個常用的.
TaskContinuationOptions.OnlyOnRanToCompletion 主要當前面的任務,完美的完成任務,才能執行延續任務.
class Program
{
static void Main(string[] args)
{
Task<int> task = Task.Run(() => ChildThreadOne());
task.ContinueWith(t => Console.WriteLine("子線程一的延續任務,只有在子線程一完美的完成的任務的情況下,才會執行"), TaskContinuationOptions.OnlyOnRanToCompletion);
Console.WriteLine("主線程繼續執行它的操作");
Console.ReadKey();//必須加這行代碼,因為Task時線程池線程,屬於后台線程
}
/// <summary>
/// 子線程一
/// </summary>
static int ChildThreadOne()
{
Thread.Sleep(2000);//模擬執行長時間計算任務
Console.WriteLine("子線程一完成了計算任務,返回值6");
return 6;
}
}

這里,看着,讓子線程一拋出異常,看看延續任務會不會繼續執行.
static void Main(string[] args)
{
Task<int> task = Task.Run(() => ChildThreadOne());
task.ContinueWith(t => Console.WriteLine("子線程一的延續任務,只有在子線程一完美的完成的任務的情況下,才會執行"), TaskContinuationOptions.OnlyOnRanToCompletion);
Console.WriteLine("主線程繼續執行它的操作");
Console.ReadKey();//必須加這行代碼,因為Task時線程池線程,屬於后台線程
}
/// <summary>
/// 子線程一
/// </summary>
static int ChildThreadOne()
{
Thread.Sleep(2000);//模擬執行長時間計算任務
Console.WriteLine("子線程一完成了計算任務,返回值6");
throw new Exception("模擬拋出異常");
}

因為子線程一拋出了異常,所以延續任務沒有執行.這里取消線程,也不會執行延續任務,因為MS為了區分Task的任務完成和任務取消,選擇讓取消的任務拋出OperationCanceledException異常,所以和拋出簡單一樣,延續任務並不會執行.超時同理.
TaskContinuationOptions.OnlyOnFaulted 當前面的任務拋出未處理的異常是,執行延續任務.
static void Main(string[] args)
{
CancellationTokenSource source = new CancellationTokenSource();
Task<int> task = Task.Run(() => ChildThreadOne(source.Token));
task.ContinueWith(t =>
{
Console.WriteLine("子線程一的延續任務,只有在子線程一拋出了未處理的異常,才會執行,這里嘗試處理拋出的異常");
//一般記日志,Logger.Error("");
task.Exception.Handle(x =>
{
Console.WriteLine("最好在這里就處理掉異常,以免讓外部try canth捕獲到,並處理產生的性能損失");
if (x is OperationCanceledException)
{
Console.WriteLine("子線程一拋出了取消異常,異常信息為{0}", x.Message);
}
else {
Console.WriteLine("子線程一拋出了一般異常,異常信息為{0}", x.Message);
}
return true;//返回true,告訴CLR異常已被處理,這樣外部try catch就捕獲不到了.
});
}, TaskContinuationOptions.OnlyOnFaulted);
Console.WriteLine("主線程繼續執行它的操作");
source.Cancel();
Console.ReadKey();//必須加這行代碼,因為Task時線程池線程,屬於后台線程
}
/// <summary>
/// 子線程一
/// </summary>
static int ChildThreadOne(CancellationToken cancellation)
{
Thread.Sleep(2000);//模擬執行長時間計算任務
Console.WriteLine("子線程一完成了計算任務,返回值6");
cancellation.ThrowIfCancellationRequested();//拋出取消異常
return 6;
}

這里建議對TaskContinueWith做一個封裝,讓它能處理不同的異常,並且這樣異常,能在內部就被全部處理掉,而不需要在外部進行try catch處理,並且有一個友好的異常記錄和輸出.這里我就不做了,有需要的可以聯系我.
ok,這里就介紹TaskContinuationOptions常用的兩個值,其余的用法都差不多,可以看Ms的提供的備注,或者參看MSDN,這里就不全介紹了.
本文屬於轉載!!!!!!!!!!
本文屬於轉載!!!!!!!!!!
本文屬於轉載!!!!!!!!!!

