改善C#程式的建議9:使用Task代替ThreadPool和Thread

NO IMAGE

原文地址為:改善C#程式的建議9:使用Task代替ThreadPool和Thread

一:Task的優勢

ThreadPool相比Thread來說具備了很多優勢,但是ThreadPool卻又存在一些使用上的不方便。比如:

1: ThreadPool不支援執行緒的取消、完成、失敗通知等互動性操作;

2: ThreadPool不支援執行緒執行的先後次序;

以往,如果開發者要實現上述功能,需要完成很多額外的工作,現在,FCL中提供了一個功能更強大的概念:Task。Task線上程池的基礎上進行了優化,並提供了更多的API。在FCL4.0中,如果我們要編寫多執行緒程式,Task顯然已經優於傳統的方式。

以下是一個簡單的任務示例:

static
void
Main(
string
[] args)
{
Task t
=
new
Task(()
=>

{
Console.WriteLine(

任務開始工作……

);
//
模擬工作過程

Thread.Sleep(
5000
);
});
t.Start();
t.ContinueWith((task)
=>

{
Console.WriteLine(

任務完成,完成時候的狀態為:

);
Console.WriteLine(

IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}

, task.IsCanceled, task.IsCompleted, task.IsFaulted);
});
Console.ReadKey();
}


二:Task的完成狀態

任務Task有這樣一些屬性,讓我們查詢任務完成時的狀態:

1: IsCanceled,因為被取消而完成;

2: IsCompleted,成功完成;

3: IsFaulted,因為發生異常而完成

需要注意的是,任務並沒有提供回撥事件來通知完成(像BackgroundWorker一樣),它通過啟用一個新任務的方式來完成類似的功能。ContinueWith方法可以在一個任務完成的時候發起一個新任務,這種方式天然就支援了任務的完成通知:我們可以在新任務中獲取原任務的結果值。

       下面是一個稍微複雜一點的例子,同時支援完成通知、取消、獲取任務返回值等功能:

static
void
Main(
string
[] args)
{
CancellationTokenSource cts
=
new
CancellationTokenSource();
Task
<
int
>
t
=
new
Task
<
int
>
(()
=>
Add(cts.Token), cts.Token);
t.Start();
t.ContinueWith(TaskEnded);
//
等待按下任意一個鍵取消任務

Console.ReadKey();
cts.Cancel();
Console.ReadKey();
}

static
void
TaskEnded(Task
<
int
>
task)
{
Console.WriteLine(

任務完成,完成時候的狀態為:

);
Console.WriteLine(

IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}

, task.IsCanceled, task.IsCompleted, task.IsFaulted);
Console.WriteLine(

任務的返回值為:{0}

, task.Result);
}

static
int
Add(CancellationToken ct)
{
Console.WriteLine(

任務開始……

);
int
result
=
0
;
while
(
!
ct.IsCancellationRequested)
{
result

;
Thread.Sleep(
1000
);
}
return
result;
}

在任務開始後大概3秒鐘的時候按下鍵盤,會得到如下的輸出:

任務開始……
任務完成,完成時候的狀態為:
IsCanceled
=
False IsCompleted
=
True IsFaulted
=
False
任務的返回值為:
3

你也許會奇怪,我們的任務是通過Cancel的方式處理,為什麼完成的狀態IsCanceled那一欄還是False。這是因為在工作任務中,我們對於IsCancellationRequested進行了業務邏輯上的處理,並沒有通過ThrowIfCancellationRequested方法進行處理。如果採用後者的方式,如下:

static
void
Main(
string
[] args)
{
CancellationTokenSource cts
=
new
CancellationTokenSource();
Task
<
int
>
t
=
new
Task
<
int
>
(()
=>
AddCancleByThrow(cts.Token), cts.Token);
t.Start();
t.ContinueWith(TaskEndedByCatch);
//
等待按下任意一個鍵取消任務

Console.ReadKey();
cts.Cancel();
Console.ReadKey();
}

static
void
TaskEndedByCatch(Task
<
int
>
task)
{
Console.WriteLine(

任務完成,完成時候的狀態為:

);
Console.WriteLine(

IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}

, task.IsCanceled, task.IsCompleted, task.IsFaulted);
try

{
Console.WriteLine(

任務的返回值為:{0}

, task.Result);
}
catch
(AggregateException e)
{
e.Handle((err)
=>
err
is
OperationCanceledException);
}
}

static
int
AddCancleByThrow(CancellationToken ct)
{
Console.WriteLine(

任務開始……

);
int
result
=
0
;
while
(
true
)
{
ct.ThrowIfCancellationRequested();
result

;
Thread.Sleep(
1000
);
}
return
result;
}

那麼輸出為:

任務開始……
任務完成,完成時候的狀態為:
IsCanceled
=
True IsCompleted
=
True IsFaulted
=
False

在任務結束求值的方法TaskEndedByCatch中,如果任務是通過ThrowIfCancellationRequested方法結束的,對任務求結果值將會丟擲異常OperationCanceledException,而不是得到丟擲異常前的結果值。這意味著任務是通過異常的方式被取消掉的,所以可以注意到上面程式碼的輸出中,狀態IsCancled為True。

再一次,我們注意到取消是通過異常的方式實現的,而表示任務中發生了異常的IsFaulted狀態卻還是等於False。這是因為ThrowIfCancellationRequested是協作式取消方式型別CancellationTokenSource的一個方法,CLR進行了特殊的處理。CLR知道這一行程式開發者有意為之的程式碼,所以不把它看作是一個異常(它被理解為取消)。要得到IsFaulted等於True的狀態,我們可以修改While迴圈,模擬一個異常出來:

while
(
true
)
{
//
ct.ThrowIfCancellationRequested();

if
(result
==
5
)
{
throw
new
Exception(

error

);
}
result

;
Thread.Sleep(
1000
);
}

模擬異常後的輸出為:

任務開始……
任務完成,完成時候的狀態為:
IsCanceled
=
False IsCompleted
=
True IsFaulted
=
True


三:任務工廠

Task還支援任務工廠的概念。任務工廠支援多個任務之間共享相同的狀態,如取消型別CancellationTokenSource就是可以被共享的。通過使用任務工廠,可以同時取消一組任務:

static
void
Main(
string
[] args)
{
CancellationTokenSource cts
=
new
CancellationTokenSource();
//
等待按下任意一個鍵取消任務

TaskFactory taskFactory
=
new
TaskFactory();
Task[] tasks
=
new
Task[]
{
taskFactory.StartNew(()
=>
Add(cts.Token)),
taskFactory.StartNew(()
=>
Add(cts.Token)),
taskFactory.StartNew(()
=>
Add(cts.Token))
};
//
CancellationToken.None指示TasksEnded不能被取消

taskFactory.ContinueWhenAll(tasks, TasksEnded, CancellationToken.None);
Console.ReadKey();
cts.Cancel();
Console.ReadKey();
}

static
void
TasksEnded(Task[] tasks)
{
Console.WriteLine(

所有任務已完成!

);
}

以上程式碼輸出為:

任務開始……
任務開始……
任務開始……
所有任務已完成(取消)!

本建議演示了Task(任務)和TaskFactory(任務工廠)的使用方法。Task甚至進一步優化了後臺執行緒池的排程,加快了執行緒的處理速度。在FCL4.0時代,使用多執行緒,我們理應更多地使用Task。

 微信掃一掃,關注最課程(www.zuikc.com),獲取更多我的文章,獲取軟體開發每日一練

之前內容:

改善程式的建議8:避免鎖定不恰當的同步物件
改善C#程式的建議7:正確停止執行緒
改善C#程式的建議6:線上程同步中使用訊號量
改善C#程式的建議5:引用型別賦值為null與加速垃圾回收
改善C#程式的建議4:C#中標準Dispose模式的實現
改善C#程式的建議3:在C#中選擇正確的集合進行編碼
改善C#程式的建議2:C#中dynamic的正確用法
改善C#程式的建議1:非用ICloneable不可的理由

轉載請註明本文地址:改善C#程式的建議9:使用Task代替ThreadPool和Thread