任務基礎一共三篇,本篇是第三篇,之後開始學習非同步程式設計、併發、非同步I/O的知識。
本篇會繼續講述 Task 的一些 API 和常用的操作。
TaskAwaiter
先說一下 TaskAwaiter
,TaskAwaiter
表示等待非同步任務完成的物件併為結果提供引數。
Task 有個 GetAwaiter()
方法,會返回TaskAwaiter
或TaskAwaiter<TResult>
,TaskAwaiter
型別在 System.Runtime.CompilerServices
名稱空間中定義。
TaskAwaiter
型別的屬性和方法如下:
屬性:
屬性 | 說明 |
---|---|
IsCompleted | 獲取一個值,該值指示非同步任務是否已完成。 |
方法:
方法 | 說明 |
---|---|
GetResult() | 結束非同步任務完成的等待。 |
OnCompleted(Action) | 將操作設定為當 TaskAwaiter 物件停止等待非同步任務完成時執行。 |
UnsafeOnCompleted(Action) | 計劃與此 awaiter 相關非同步任務的延續操作。 |
使用示例如下:
static void Main()
{
Task<int> task = new Task<int>(()=>
{
Console.WriteLine("我是前驅任務");
Thread.Sleep(TimeSpan.FromSeconds(1));
return 666;
});
TaskAwaiter<int> awaiter = task.GetAwaiter();
awaiter.OnCompleted(()=>
{
Console.WriteLine("前驅任務完成時,我就會繼續執行");
});
task.Start();
Console.ReadKey();
}
另外,我們前面提到過,任務發生未經處理的異常,任務被終止,也算完成任務。
延續的另一種方法
上一節我們介紹了 .ContinueWith()
方法來實現延續,這裡我們介紹另一個延續方法 .ConfigureAwait()
。
.ConfigureAwait()
如果要嘗試將延續任務封送回原始上下文,則為 true
;否則為 false
。
我來解釋一下, .ContinueWith()
延續的任務,當前驅任務完成後,延續任務會繼續在此執行緒上繼續執行。這種方式是同步的,前者和後者連續在一個執行緒上執行。
.ConfigureAwait(false)
方法可以實現非同步,前驅方法完成後,可以不理會後續任務,而且後續任務可以在任意一個執行緒上執行。這個特性在 UI 介面程式上特別有用。
其使用方法如下:
static void Main()
{
Task<int> task = new Task<int>(()=>
{
Console.WriteLine("我是前驅任務");
Thread.Sleep(TimeSpan.FromSeconds(1));
return 666;
});
ConfiguredTaskAwaitable<int>.ConfiguredTaskAwaiter awaiter = task.ConfigureAwait(false).GetAwaiter();
awaiter.OnCompleted(()=>
{
Console.WriteLine("前驅任務完成時,我就會繼續執行");
});
task.Start();
Console.ReadKey();
}
ConfiguredTaskAwaitable<int>.ConfiguredTaskAwaiter
擁有跟 TaskAwaiter
一樣的屬性和方法。
.ContinueWith()
跟 .ConfigureAwait(false)
還有一個區別就是 前者可以延續多個任務和延續任務的任務(多層)。後者只能延續一層任務(一層可以有多個任務)。
另一種建立任務的方法
前面提到提到過,建立任務的三種方法:new Task()
、Task.Run()
、Task.Factory.SatrtNew()
,現在來學習第四種方法:TaskCompletionSource<TResult>
型別。
我們來看看 TaskCompletionSource<TResulr>
型別的屬性和方法:
屬性:
屬性 | 說明 |
---|---|
Task | 獲取由此 Task 建立的 TaskCompletionSource。 |
方法:
方法 | 說明 |
---|---|
SetCanceled() | 將基礎 Task 轉換為 Canceled 狀態。 |
SetException(Exception) | 將基礎 Task 轉換為 Faulted 狀態,並將其繫結到一個指定異常上。 |
SetException(IEnumerable) | 將基礎 Task 轉換為 Faulted 狀態,並對其繫結一些異常物件。 |
SetResult(TResult) | 將基礎 Task 轉換為 RanToCompletion 狀態。 |
TrySetCanceled() | 嘗試將基礎 Task 轉換為 Canceled 狀態。 |
TrySetCanceled(CancellationToken) | 嘗試將基礎 Task 轉換為 Canceled 狀態並啟用要儲存在取消的任務中的取消標記。 |
TrySetException(Exception) | 嘗試將基礎 Task 轉換為 Faulted 狀態,並將其繫結到一個指定異常上。 |
TrySetException(IEnumerable) | 嘗試將基礎 Task 轉換為 Faulted 狀態,並對其繫結一些異常物件。 |
TrySetResult(TResult) | 嘗試將基礎 Task 轉換為 RanToCompletion 狀態。 |
TaskCompletionSource<TResulr>
類可以對任務的生命週期做控制。
首先要通過 .Task
屬性,獲得一個 Task
或 Task<TResult>
。
TaskCompletionSource<int> task = new TaskCompletionSource<int>();
Task<int> myTask = task.Task; // Task myTask = task.Task;
然後通過 task.xxx()
方法來控制 myTask
的生命週期,但是呢,myTask 本身是沒有任務內容的。
使用示例如下:
static void Main()
{
TaskCompletionSource<int> task = new TaskCompletionSource<int>();
Task<int> myTask = task.Task; // task 控制 myTask
// 新開一個任務做實驗
Task mainTask = new Task(() =>
{
Console.WriteLine("我可以控制 myTask 任務");
Console.WriteLine("按下任意鍵,我讓 myTask 任務立即完成");
Console.ReadKey();
task.SetResult(666);
});
mainTask.Start();
Console.WriteLine("開始等待 myTask 返回結果");
Console.WriteLine(myTask.Result);
Console.WriteLine("結束");
Console.ReadKey();
}
其它例如 SetException(Exception)
等方法,可以自行探索,這裡就不再贅述。
參考資料:https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/
這篇文章講得不錯,而且有圖:https://gigi.nullneuron.net/gigilabs/taskcompletionsource-by-example/
實現一個支援同步和非同步任務的型別
這部分內容對 TaskCompletionSource<TResult>
繼續進行講解。
這裡我們來設計一個類似 Task 型別的類,支援同步和非同步任務。
- 使用者可以使用
GetResult()
同步獲取結果; - 使用者可以使用
RunAsync()
執行任務,使用.Result
屬性非同步獲取結果;
其實現如下:
/// <summary>
/// 實現同步任務和非同步任務的型別
/// </summary>
/// <typeparam name="TResult"></typeparam>
public class MyTaskClass<TResult>
{
private readonly TaskCompletionSource<TResult> source = new TaskCompletionSource<TResult>();
private Task<TResult> task;
// 儲存使用者需要執行的任務
private Func<TResult> _func;
// 是否已經執行完成,同步或非同步執行都行
private bool isCompleted = false;
// 任務執行結果
private TResult _result;
/// <summary>
/// 獲取執行結果
/// </summary>
public TResult Result
{
get
{
if (isCompleted)
return _result;
else return task.Result;
}
}
public MyTaskClass(Func<TResult> func)
{
_func = func;
task = source.Task;
}
/// <summary>
/// 同步方法獲取結果
/// </summary>
/// <returns></returns>
public TResult GetResult()
{
_result = _func.Invoke();
isCompleted = true;
return _result;
}
/// <summary>
/// 非同步執行任務
/// </summary>
public void RunAsync()
{
Task.Factory.StartNew(() =>
{
source.SetResult(_func.Invoke());
isCompleted = true;
});
}
}
我們在 Main 方法中,建立任務示例:
class Program
{
static void Main()
{
// 例項化任務類
MyTaskClass<string> myTask1 = new MyTaskClass<string>(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(1));
return "www.whuanle.cn";
});
// 直接同步獲取結果
Console.WriteLine(myTask1.GetResult());
// 例項化任務類
MyTaskClass<string> myTask2 = new MyTaskClass<string>(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(1));
return "www.whuanle.cn";
});
// 非同步獲取結果
myTask2.RunAsync();
Console.WriteLine(myTask2.Result);
Console.ReadKey();
}
}
Task.FromCanceled()
微軟文件解釋:建立 Task,它因指定的取消標記進行的取消操作而完成。
這裡筆者抄來了一個示例:
var token = new CancellationToken(true);
Task task = Task.FromCanceled(token);
Task<int> genericTask = Task.FromCanceled<int>(token);
網上很多這樣的示例,但是,這個東西到底用來幹嘛的?new 就行了?
帶著疑問我們來探究一下,來個示例:
public static Task Test()
{
CancellationTokenSource source = new CancellationTokenSource();
source.Cancel();
return Task.FromCanceled<object>(source.Token);
}
static void Main()
{
var t = Test(); // 在此設定斷點,監控變數
Console.WriteLine(t.IsCanceled);
}
Task.FromCanceled()
可以構造一個被取消的任務。我找了很久,沒有找到很好的示例,如果一個任務在開始前就被取消,那麼使用 Task.FromCanceled()
是很不錯的。
如何在內部取消任務
之前我們討論過,使用 CancellationToken
取消令牌傳遞引數,使任務取消。但是都是從外部傳遞的,這裡來實現無需 CancellationToken
就能取消任務。
我們可以使用 CancellationToken
的 ThrowIfCancellationRequested()
方法丟擲 System.OperationCanceledException
異常,然後終止任務,任務會變成取消狀態,不過任務需要先傳入一個令牌。
這裡筆者來設計一個難一點的東西,一個可以按順序執行多個任務的類。
示例如下:
/// <summary>
/// 能夠完成多個任務的非同步型別
/// </summary>
public class MyTaskClass
{
private List<Action> _actions = new List<Action>();
private CancellationTokenSource _source = new CancellationTokenSource();
private CancellationTokenSource _sourceBak = new CancellationTokenSource();
private Task _task;
/// <summary>
/// 新增一個任務
/// </summary>
/// <param name="action"></param>
public void AddTask(Action action)
{
_actions.Add(action);
}
/// <summary>
/// 開始執行任務
/// </summary>
/// <returns></returns>
public Task StartAsync()
{
// _ = new Task() 對本示例無效
_task = Task.Factory.StartNew(() =>
{
for (int i = 0; i < _actions.Count; i++)
{
int tmp = i;
Console.WriteLine($"第 {tmp} 個任務");
if (_source.Token.IsCancellationRequested)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("任務已經被取消");
Console.ForegroundColor = ConsoleColor.White;
_sourceBak.Cancel();
_sourceBak.Token.ThrowIfCancellationRequested();
}
_actions[tmp].Invoke();
}
},_sourceBak.Token);
return _task;
}
/// <summary>
/// 取消任務
/// </summary>
/// <returns></returns>
public Task Cancel()
{
_source.Cancel();
// 這裡可以省去
_task = Task.FromCanceled<object>(_source.Token);
return _task;
}
}
Main 方法中:
static void Main()
{
// 例項化任務類
MyTaskClass myTask = new MyTaskClass();
for (int i = 0; i < 10; i++)
{
int tmp = i;
myTask.AddTask(() =>
{
Console.WriteLine(" 任務 1 Start");
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine(" 任務 1 End");
Thread.Sleep(TimeSpan.FromSeconds(1));
});
}
// 相當於 Task.WhenAll()
Task task = myTask.StartAsync();
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine($"任務是否被取消:{task.IsCanceled}");
// 取消任務
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("按下任意鍵可以取消任務");
Console.ForegroundColor = ConsoleColor.White;
Console.ReadKey();
var t = myTask.Cancel(); // 取消任務
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine($"任務是否被取消:【{task.IsCanceled}】");
Console.ReadKey();
}
你可以在任一階段取消任務。
Yield 關鍵字
迭代器關鍵字,使得資料不需要一次性返回,可以在需要的時候一條條迭代,這個也相當於非同步。
迭代器方法執行到 yield return
語句時,會返回一個 expression
,並保留當前在程式碼中的位置。 下次呼叫迭代器函式時,將從該位置重新開始執行。
可以使用 yield break
語句來終止迭代。
官方文件:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/yield
網上的示例大多數都是 foreach
的,有些同學不理解這個到底是啥意思。筆者這裡簡單說明一下。
我們也可以這樣寫一個示例:
這裡已經沒有 foreach
了。
private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
private static IEnumerable<int> ForAsync()
{
int i = 0;
while (i < list.Length)
{
i++;
yield return list[i];
}
}
但是,同學又問,這個 return 返回的物件 要實現這個 IEnumerable<T>
才行嘛?那些文件說到什麼迭代器介面什麼的,又是什麼東西呢?
我們可以先來改一下示例:
private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
private static IEnumerable<int> ForAsync()
{
int i = 0;
while (i < list.Length)
{
int num = list[i];
i++;
yield return num;
}
}
你在 Main 方法中呼叫,看看是不是正常執行?
static void Main()
{
foreach (var item in ForAsync())
{
Console.WriteLine(item);
}
Console.ReadKey();
}
這樣說明了,yield return
返回的物件,並不需要實現 IEnumerable<int>
方法。
其實 yield
是語法糖關鍵字,你只要在迴圈中呼叫它就行了。
static void Main()
{
foreach (var item in ForAsync())
{
Console.WriteLine(item);
}
Console.ReadKey();
}
private static IEnumerable<int> ForAsync()
{
int i = 0;
while (i < 100)
{
i++;
yield return i;
}
}
}
它會自動生成 IEnumerable<T>
,而不需要你先實現 IEnumerable<T>
。
補充知識點
-
執行緒同步有多種方法:臨界區(Critical Section)、互斥量(Mutex)、訊號量(Semaphores)、事件(Event)、任務(Task);
-
Task.Run()
和Task.Factory.StartNew()
封裝了 Task; -
Task.Run()
是Task.Factory.StartNew()
的簡化形式; -
有些地方
net Task()
是無效的;但是Task.Run()
和Task.Factory.StartNew()
可以;
本篇是任務基礎的終結篇,至此 C# 多執行緒系列,一共完成了 15 篇,後面會繼續深入多執行緒和任務的更多使用方法和場景。
喜歡我的作者記得關注我喲~