任務一般是一些比較耗時的操作(IO或者複雜計算),如果在主線程運行,將影響程序的流暢性。所以,我們一般會新建線程處理任務。.NET4開始引進了Task,它對Thread做了大量方便易用的封裝。我們將詳細講述Task的使用方法,以及各種多線程的使用場合。
一、任務設計
1.1 基本設計
一個簡單的耗時操作如下所示:
void Func1()
{
Thread.Sleep(1000);
}
如果有返回值,將如下所示:
int Func2()
{
Thread.Sleep(1500);
return 1;
}
耗時操作如果要新建線程去運行,可以這麼寫:
Task task1 = new Task(Func1);
task1.Start();
Task<int> task2 = new Task<int>(Func2);
task2.Start();
C#5.0引入了async、await這些關鍵字,異步方法的寫法爲:
async Task Func1()
{
await Task.Delay(1000);
}
async Task<int> Func2()
{
await Task.Delay(1500);
return 1;
}
調用時就像同步的方法一樣:
Func1();
Func2();
需要注意的是,雖然代碼看上去是先執行Func1,然後再執行Func2,但實際上,這兩個都是異步方法,它們會同時開始執行。
1.2 可取消的任務
在使用Thread時會看到一個Abort方法,用來終止線程。但其實這個方法非常不安全,甚至不能終止,一般不提倡使用。事實上,如果一個耗時方法是我們自己設計的,我們可以想辦法在中途退出;而如果這個方法我們只能調用,無法修改,那這個任務基本無法中止。
假設有一個耗時方法如下所示:
async Task Func1()
{
await Task.Delay(10000);
}
一旦調用,這個方法將運行10秒。爲了避免這種情況,我們需要對等待進行切割。如:
async Task Func1()
{
for (int i = 0; i < 100; i++)
{
await Task.Delay(100);
}
}
如果希望中途能夠取消,那麼可以寫成:
async Task Func1(CancellationToken cancellationToken)
{
for (int i = 0; i < 100; i++)
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
await Task.Delay(100);
}
}
調用的方法如下:
CancellationTokenSource cts = new CancellationTokenSource();
await Func1(cts.Token);
如果需要在另一個線程裏面取消,只需要調用以下語句即可:
cts.Cancel();
1.3 進度報告
有時候我們需要實時知道耗時任務的運行進度,例如下載,可能需要向用戶提供一個完成百分比。
帶進度報告功能的異步任務設計如下:
async Task Func1(IProgress<int> progress)
{
for (int i = 0; i < 100; i++)
{
await Task.Delay(100);
progress.Report(i + 1);
}
}
調用方法則如下所示:
await Func1(new Progress<int>(p =>
{
Console.WriteLine(p);
}));
二、單任務使用
對於沒有返回值的任務,我們上面已經提到了使用的方法。如果任務有返回值,而我們需要使用這個返回值,調用的方法如下:
(1)使用async、await關鍵字
async Task<int> Func2()
{
await Task.Delay(1500);
return 1;
}
//調用方法
int result = await Func2();
(2)不使用async、await關鍵字
int Func2()
{
Thread.Sleep(1500);
return 1;
}
//調用方法
Task<int> task = new Task<int>(Func2);
task.ContinueWith(t =>
{
int result = t.Result;
});
三、多任務使用
3.1 所有任務完成後處理
使用WhenAll方法,使用方法如下:
async Task<int> Func1()
{
await Task.Delay(1000);
return 1;
}
async Task<int> Func2()
{
await Task.Delay(1500);
return 2;
}
Task<int>[] tasks = new Task<int>[] { Func1(), Func2() };
int[] results = await Task.WhenAll(tasks);
3.2 任一任務完成即處理
使用WhenAny方法,使用方法如下:
async Task<int> Func1()
{
await Task.Delay(1000);
return 1;
}
async Task<int> Func2()
{
await Task.Delay(1500);
return 2;
}
Task<int>[] tasks = new Task<int>[] { Func1(), Func2() };
Task<int> task = await Task.WhenAny(tasks);
int result = task.Result;
WhenAny方法可以有以下的應用場景:
(1)超時判斷。寫一個簡單的延時任務,跟需要執行的任務一同放在列表裏。如果WhenAny先返回延時任務,說明需要執行的任務已經超時了。
(2)一個任務完成,取消執行餘下任務。結合CancellationToken完成這一功能,具體代碼如下:
async Task Func1(CancellationToken cancellationToken)
{
for (int i = 0; i < 20; i++)
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
await Task.Delay(100);
}
}
async Task Func2(CancellationToken cancellationToken)
{
for (int i = 0; i < 100; i++)
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
await Task.Delay(110);
}
}
CancellationTokenSource cts = new CancellationTokenSource();
Task[] tasks = new Task[] { Func1(cts.Token), Func2(cts.Token) };
Task task = await Task.WhenAny(tasks);
cts.Cancel();