# .NET Core 6 Task 筆記 以下都以 Dapper 查詢資料庫為例,資料庫有兩個資料表,一個為 Products 有文字的欄位 Name,另一個為 Employees 有數字的欄位 Salary。 # 基本 async await ## 同步方法 `using Dapper;` 後,`SqlConnection` 連線物件可用的方法中,早期查詢用 `Query` 或 `Query<T>`,傳回的是多筆 `IEnumerable<T>`,後面接 `.ToList()` 來運用,這是 ==同步== 的方法,也就是一列一列程式碼往下走,還沒得到回應就會停在那裡,直到做完才會往後繼續。 下面程式碼的方法 GetProductName,會撈出 Products 資料表的所有 Name 資料,用頓號 `、` 串起來傳回來,我們再把文字再顯示在螢幕上,而 `WAITFOR DELAY '00:00:03';` 是用來模擬網路延遲,可觀察螢幕出現「開始找商品名稱」後的等待時間,會明顯等最少三秒,取得結果才會印出串起來的文字。 ```csharp= using Dapper; using System.Data.SqlClient; Console.WriteLine("開始找商品名稱"); string names = GetProductName(); Console.WriteLine("商品一覽:" + names); Console.ReadKey(); string GetProductName() { string sql = "WAITFOR DELAY '00:00:03'; SELECT [Name] FROM [Products]"; IList<string> products; using(SqlConnection conn = new SqlConnection("...")) { products = conn.Query<string>(sql).ToList(); } return string.Join("、", products); } ``` ## 非同步方法 其實 Dapper 的方法還有一套名稱帶有 Async 的,例如 `QueryAsync`、`ExecuteAsync`,VisualStudio 將「非同步」翻做「==可等待==」,會變得比較好理解,這種「可等待」的非同步方法讓我們可以「不等待」,工作效率就會提高,產能變好,相較起來以前的同步方法就是「強迫等待」了。 但要使用非同步方法,會有一系統東西要改,以 Dapper 的 `QueryAsync` 為例,方法不是回傳 `IEnumerable<T>` 可直接做 `.ToList()`,而是回傳 `Task<IEnumerable<T>>`,剛開始可能會被這個回傳的東西嚇到,但看完筆記再回來看這段,就會覺得沒什麼了。先看程式碼: ```csharp= using Dapper; using System.Data.SqlClient; Console.WriteLine("開始找商品名稱"); string names = await GetProductName(); Console.WriteLine("商品一覽:" + names); Console.ReadKey(); async Task<string> GetProductName() { string sql = "WAITFOR DELAY '00:00:03'; SELECT [Name] FROM [Products]"; IList<string> products; using(SqlConnection conn = new SqlConnection("...")) { products = (await conn.QueryAsync<string>(sql)).ToList(); } return string.Join("、", products); } ``` 變更的點有: * (16列) QueryAsync 方法前面要加上 `await` 修飾,表示要等待這個查詢動作。 * (16列) 查詢結果一樣可以做 `.ToList()` 但要用括號把整個 `await conn.QueryAsync<string>(sql)` 包起來再 `.ToList()`。 * (9列) 方法要加上 `async` 宣告非同步「可等待」的修飾詞。 * (9列) 方法回傳的類型從 `string` 改成 `Task<string>`,可想成「**回傳一個工作 Task,這個工作夾帶著 string**」 * (5列) 呼叫這個可等待的非同步方法 GetProductName 時要加 `await`,表示等待這個 Task 完成,取得 string。 結果和前面的同步方法是完全一樣的,也是顯示「開始找商品名稱」後停滯三秒後,用頓號列出商品名稱。 # 可等待但不等待 上面的兩個範例看不出使用 Task 的好處,我們換個做法,實作上常常要帶回很多資料,只回傳單一的類型不敷使用。所以我會用一個物件來攜帶要取回的資料,然後回傳用 `void`。這裡要傳遞用的物件我叫 Carry: ```csharp= class Carry { internal IList<string> Products { get; set; } = new List<string>(); internal IList<int> Employees { get; set; } = new List<int>(); } ``` ## 同步 古早的同步寫法還是先來一段,下面是查詢出 Products 的所有 Name,還有查詢出 Employees 的所有 Salary。都是使用 Dapper 的 `Query<T>` 同步方法。 ```csharp= using Dapper; using System.Data.SqlClient; Carry carry = new Carry(); Console.WriteLine("開始找商品名稱"); GetProductName(carry); Console.WriteLine("開始找職員薪水"); GetEmployeeSalary(carry); Console.WriteLine("商品一覽:" + string.Join("、", carry.Products)); Console.WriteLine("薪水總和:" + carry.Employees.Sum()); Console.ReadKey(); void GetProductName(Carry carry) { string sql = "WAITFOR DELAY '00:00:03'; SELECT [Name] FROM [Products]"; IList<string> products; using(SqlConnection conn = new SqlConnection("...")) { products = conn.Query<string>(sql).ToList(); } carry.Products = products; return; } void GetEmployeeSalary(Carry carry) { string sql = "WAITFOR DELAY '00:00:02'; SELECT [Salary] FROM [Employees]"; IList<int> employees; using (SqlConnection conn = new SqlConnection("...")) { employees = conn.Query<int>(sql).ToList(); } carry.Employees = employees; return; } ``` 以上兩個 SQL 語句都有模擬延遲,會看到查詢商品名稱花了三秒,然後查詢職員薪水花了兩秒,也就是整整經過五秒才會印出最後結果。 ## 非同步 使用 `QueryAsync<T>` 後,兩個查詢方法 GetProductName 和 GetEmployeeSalary 要加上 `async` 修飾詞讓他非同步「可等待」,但因為沒有回傳,所以將 `void` 改成 `Task` 就好,以下程式碼: ```csharp= using Dapper; using System.Data.SqlClient; Carry carry = new Carry(); Console.WriteLine("開始找商品名稱"); await GetProductName(carry); Console.WriteLine("開始找職員薪水"); await GetEmployeeSalary(carry); Console.WriteLine("商品一覽:" + string.Join("、", carry.Products)); Console.WriteLine("薪水總和:" + carry.Employees.Sum()); Console.ReadKey(); async Task GetProductName(Carry carry) { string sql = "WAITFOR DELAY '00:00:03'; SELECT [Name] FROM [Products]"; IList<string> products; using(SqlConnection conn = new SqlConnection("...")) { products = (await conn.QueryAsync<string>(sql)).ToList(); } carry.Products = products; return; } async Task GetEmployeeSalary(Carry carry) { string sql = "WAITFOR DELAY '00:00:02'; SELECT [Salary] FROM [Employees]"; IList<int> employees; using (SqlConnection conn = new SqlConnection("...")) { employees = (await conn.QueryAsync<int>(sql)).ToList(); } carry.Employees = employees; return; } ``` 變更點: * (24列 37列) 改成 `QueryAsync` 後,前面加上 `await`,整個包起來再 `.ToList()`。 * (17列 30列) 加上 `async` 宣告可等待,回傳從 `void` 改成 `Task`(其實維持 `void` 還是可編譯成功,但就沒有可等待效果)。 * (7列 10列) 呼叫時加上 `await` 等待工作結束。 這結果與上面同步結果一樣,查詢商品名稱花費三秒,查詢職員薪水花了兩秒,全部要花五秒才看到最後結果。 ## 非同步但不等待 試著將上面的 7 列和 10 列維持原來直接呼叫的寫法,**不加上** `await` 修飾: ```csharp= using Dapper; using System.Data.SqlClient; Carry carry = new Carry(); Console.WriteLine("開始找商品名稱"); GetProductName(carry); Console.WriteLine("開始找職員薪水"); GetEmployeeSalary(carry); /* ... 後面省略 ... */ ``` 會 **發現 IDE 有警告,還是可編譯可執行**,然而執行結果是 ==瞬間結束,商品一覽為空白,薪水總和為 0== 。原因很明顯,因為呼叫時沒有加 `await` 所以不等待 Task 完成,就一直往下執行而印出空 List 裡的名稱字串和空 List 裡薪水總和。 一般來說實務不會希望這樣,這裡只是故意引發這個情況,來瞭解 `await` 的功用。 # 等待所有工作結束 很多時候我們希望 **可分開做的事情同時去做**,例如上面花三秒查詢商品,花兩秒查詢薪水,能不能同時做、在兩個工作都完成時印出結果呢?這就要用到 `Task.WaitAll` 或 `Task.WhenAll` 方法了。 ## Task 物件 在用這個靜態方法之前,要先知道 GetProductName 與 GetEmployeeSalary 方法加上 `async` 時,回傳的是 `Task`,所以可以宣告一個 Task 物件來接 GetProductName 的回傳,可以先看這個物件有什麼內容。 ```csharp= using Dapper; using System.Data.SqlClient; Carry carry = new Carry(); Console.WriteLine("開始找商品名稱"); Task taskP = GetProductName(carry); while (true) { if (taskP.IsCompleted) { Console.WriteLine("商品一覽:" + string.Join("、", carry.Products)); break; } else { Console.Write("*"); } } Console.ReadKey(); async Task GetProductName(Carry carry) { string sql = "SELECT [Name] FROM [Products]"; IList<string> products; using(SqlConnection conn = new SqlConnection("...")) { products = (await conn.QueryAsync<string>(sql)).ToList(); } carry.Products = products; return; } ``` 上面程式碼範例 9 列到 21 列的 `while` 是粗暴了點,但可以觀察 `Task` 物件 taskP 狀態的變化,7 列開始執行後就直接往下繼續了,進入 while 迴圈後持續印出 `*` 在螢幕上,直到 SQL 查詢完成後,taskP 的 `IsCompleted` 會變為 `true`,這時迴圈進入 13 列,印出商品一覽並跳出。為避免印出太多 `*`,原本 SQL 語句有 `WAITFOR DELAY` 的部份這裡暫時拿掉了,有 `*` 應該就可以看出 SQL 查詢還是有微量的時間花費。 ## Task&lt;T&gt; 如果是最早有回傳 `string` 的寫法,加上 `async` 後則是回傳 `Task<string>`, ```csharp= using Dapper; using System.Data.SqlClient; Console.WriteLine("開始找商品名稱"); Task<string> taskP = GetProductName(); while (true) { if (taskP.IsCompleted) { Console.WriteLine("商品一覽:" + taskP.Result); break; } else { Console.Write("N"); } } Console.ReadKey(); async Task<string> GetProductName() { string sql = "SELECT [Name] FROM [Products]"; IList<string> products; using(SqlConnection conn = new SqlConnection("...")) { products = (await conn.QueryAsync<string>(sql)).ToList(); } return string.Join("、", products); } ``` 以上程式碼是將 GetProductName 方法改成最早的回傳字串版本,相對應的調整如下: * (31 列、23 列) 由於回傳字串又有 `async` 宣告可等待,所以回傳為 `Task<string>` * (7 列) 宣告 `Task<string>` 的 `taskP` 來接 GetProductName 的工作 * (13 列) 使用 `.Result` 來取得 `Task<T>` 的 `T`,在這裡是取得回傳的 `string` 結果 ## 靜態方法 Task.WaitAll 如果有多個 Task ,用靜態方法 `Task.WaitAll` 可等待這些工作都完成了,才會繼續往下做,他的引數是陣列的 Task,所以是 `Task[]`,可以要呼叫使用的時候傳入 `new Task[] { task1, task2, task3 }`,也可以先用一個 `List<Task>` 分別 `.Add` 最後再 `.ToArray` 傳入,下面示範用 `new Task[]` 的做法: ```csharp= using Dapper; using System.Data.SqlClient; Carry carry = new Carry(); Console.WriteLine("開始找商品名稱"); Task<string> taskP = GetProductName(); Console.WriteLine("開始找職員薪水"); Task taskE = GetEmployeeSalary(carry); Task.WaitAll(new Task[] { taskP, taskE }); Console.WriteLine("商品一覽:" + taskP.Result); Console.WriteLine("薪水總和:" + carry.Employees.Sum()); Console.ReadKey(); async Task<string> GetProductName() { string sql = "WAITFOR DELAY '00:00:03'; SELECT [Name] FROM [Products]"; IList<string> products; using(SqlConnection conn = new SqlConnection("...")) { products = (await conn.QueryAsync<string>(sql)).ToList(); } return string.Join("、", products); } async Task GetEmployeeSalary(Carry carry) { string sql = "WAITFOR DELAY '00:00:02'; SELECT [Salary] FROM [Employees]"; IList<int> employees; using (SqlConnection conn = new SqlConnection("...")) { employees = (await conn.QueryAsync<int>(sql)).ToList(); } carry.Employees = employees; return; } ``` * (19 列、27 列) GetProductName 方法用回傳 `string` 的做法,所以使用 `async` 後回傳物件用 `Task<string>`。 * (30 列、38 列) GetEmployeeSalary 方法無回傳,靠傳入的 `Carry` 物件來取得結果,所以使用 `async` 後回傳物件用 `Task`。 * (21 列、32 列) SQL 語句模擬網路延遲,查詢商品名稱花費三秒,查詢職員薪水花費兩秒。 * (7 列、10 列) 用 `Task<string>` 的 taskP 去接 GetProductName 方法的回傳工作,用 `Task` 的 taskE 接 GetEmployeeSalary 方法的回傳工作。 * (12 列) 使用 `Task.WaitAll` 方法,等待 taskP 和 taskE 兩個 `Task` 都完成,才會往下繼續 14、15 列,所以 14 列的 `.Result` 保證會有回傳的商品字串,15 列的 `.Employees` List 才不會是空的。 但是執行流程一直停在 `Task.WaitAll` **會有永遠離不開的風險**,如果其中一個 Task 出了什麼差錯就全部都在等他了,為預防這種可能,可以加入第二個引數為 `int` 的毫秒數,當等待了這個毫秒數後若有 Task 還沒有完成,則會回傳 `false`,如果全部完成了則回傳 `true`,可以試著改寫以上,成為: ```csharp= if(Task.WaitAll(new Task[] { taskP, taskE }, 2500)) { Console.WriteLine("工作都完成了"); } else { Console.WriteLine("有工作未完成"); } ``` 因為查詢商品名稱花費三秒,所以等待 2500 毫秒也就是 2.5 秒,只有查詢薪水的工作完成,就會印出「有工作未完成」,可以給 `Task.WaitAll` 加一個防逾時的毫秒數,當期限到了還有未完成工作,可以再控制程式的流程,例如 `throw` 例外等等後續處理。 ## 靜態方法 Task.WhenAll 上述的 `Task.WaitAll` 方法沒有回傳,加了等待毫秒數會有 `bool` 的回傳,比較難控制,還有另一個靜態方法 `Task.WhenAll`,會回傳一個 `Task` 代表傳入的 `Task[]` 工作陣列的整體執行狀態,而這個 `Task` 物件有一個方法 `.Wait()` 可以等待,以下示範程式碼: ```csharp= using Dapper; using System.Data.SqlClient; Carry carry = new Carry(); Console.WriteLine("開始找商品名稱"); Task<string> taskP = GetProductName(); Console.WriteLine("開始找職員薪水"); Task taskE = GetEmployeeSalary(carry); Task allCompleted = Task.WhenAll(new Task[] { taskP, taskE }); allCompleted.Wait(); Console.WriteLine("商品一覽:" + taskP.Result); Console.WriteLine("薪水總和:" + carry.Employees.Sum()); Console.ReadKey(); /* ... 以下省略 ... */ ``` 有網友分享 `Task.WhenAll` 比 `Task.WaitAll` 還快,不過我自己統計是覺得不相上下,但 `Task.WhenAll` 會回傳一個 `Task`,也方便後續控制,例如要做 `.ContinueWith()`,下面會介紹。 ## Task.WaitAny 和 Task.WhenAny 另外還有 Any 版本的靜態方法 `Task.WaitAny` 和 `Task.WhenAny`,等待一堆工作**當其中一個完成就繼續**,但我沒有實作的機會,也一時沒有想到明瞭的範例,所以就沒有這個的筆記了,但如果要使用應該用法也差不多。 其實 `Task.WhenAll` 就像 JavaScript 的 `Promise.all`,而 `Task.WhenAny` 就像 `Promise.race`。 # 利用 Task 多工 上面所提的是從 Dapper 的資料庫查詢,如 `QueryAsync` 或 `ExecuteAsync` 等非同步方法,延伸到討論 Task 如何操作,但其實不是一定要有 await 某個 async 方法才能使用 Task,例如下面兩個方法 ShowPrime1 和 ShowPrime2 傳入 `int` n 會印出前 n 個質數,而 ShowPrimes2 會回傳一個總結字串: ```csharp= ShowPrime1(200); string result = ShowPrime2(300); Console.WriteLine("ShowPrime2 總結為:" + result); Console.ReadKey(); void ShowPrime1(int count) { Console.WriteLine(2); if(count < 2) { return; } IList<int> list = new List<int>(); list.Add(2); int next = 3; while(list.Count < count) { bool isDivided = false; foreach(int d in list) { if (d > Math.Sqrt(next)) break; if(next % d == 0) { isDivided = true; break; } } if (!isDivided) { list.Add(next); Console.WriteLine(next); } next += 2; } return; } string ShowPrime2(int count) { Stopwatch timer = new Stopwatch(); timer.Start(); Console.WriteLine("\t" +2); if (count < 2) { timer.Stop(); return string.Format("第{0}個質數是{1}花費{2}毫秒", 1, 2, timer.ElapsedMilliseconds); } IList<int> list = new List<int>(); list.Add(2); int next = 3; while (list.Count < count) { bool isDivided = false; foreach (int d in list) { if (d > Math.Sqrt(next)) break; if (next % d == 0) { isDivided = true; break; } } if (!isDivided) { list.Add(next); Console.WriteLine("\t" + next); } next += 2; } timer.Stop(); return string.Format("第{0}個質數是{1}花費{2}毫秒", count, list[list.Count - 1], timer.ElapsedMilliseconds); } ``` 上面兩個方法的差別有兩點,第一個是 72 列 ShowPrime2 有回傳一個總結字串,第二個是 32 列和 67 列 `Console.WriteLine` 在 ShowPrime2 時有加上 `\t`,可以很明顯看出哪一列是 ShowPrime1 印的,哪一列是 ShowPrime2 印的。 而直接呼叫的結果就是同步,沒有縮排的先印完,才會印有縮排的。 ``` 2 3 5 7 . . . 1213 1217 1223 2 3 5 7 . . . 1973 1979 1987 ShowPrime2 總結為:第300個質數是1987花費119毫秒 ``` ## 靜態方法 Task.Run 靜態方法 `Task.Run` 可以創造一個 `Task` 物件,以多執行緒來執行引數的委派方法,將以上的直接呼叫,改成: ```csharp= Task.Run(() => ShowPrime1(200)); Task.Run(() => ShowPrime2(300)); Console.ReadKey(); ``` 印出來的就會多執行緒來交錯印出,例如: ``` 2 2 3 5 3 7 5 7 . . . 1193 1201 1933 1213 1217 1223 1949 1951 1973 1979 1987 ``` 而且 `Task.Run` 方法可得到 `Task` 工作物件,表示可以用前面的功能,像是取得回傳值。以下示範傳入 200 和 300,利用 `Task.WhenAll` 等待兩個工作都完成,印出 ShowPrime2 的回傳字串: ```csharp= Task task1 = Task.Run(() => ShowPrime1(200)); Task<string> task2 = Task.Run(() => ShowPrime2(300)); Task.WhenAll(task1, task2).Wait(); Console.WriteLine("ShowPrime2 總結為:" + task2.Result); Console.ReadKey(); void ShowPrime1(int count) { /* ... 省略 ... */ } string ShowPrime2(int count) { /* ... 省略 ... */ } ``` 上面可以看到 ShowPrime2 的簽章仍然是回傳 `string` 並不是 `async Task<string>`,也是可以利用 `Task.Run` 達到多工。 ``` 2 2 3 5 3 7 5 7 . . . 1193 1201 1933 1213 1217 1223 1949 1951 1973 1979 1987 ShowPrime2 總結為:第300個質數是1987花費207毫秒 ``` ## 物件方法 Start 上面靜態方法 `Task.Run` 是馬上就開始執行,若沒有要馬上開始,可以用 `new` 一個 `Task` 物件出來,在適當時機用 `.Start()` 來開始執行,示範如下: ```csharp= Task task1 = new Task(() => ShowPrime1(200)); Task<string> task2 = new Task<string>(() => ShowPrime2(300)); task1.Start(); task2.Start(); Task.WhenAll(task1, task2).Wait(); Console.WriteLine("ShowPrime2 總結為:" + task2.Result); Console.ReadKey(); ``` * (1 列、2 列) `new Task()` 建構子傳入的就是要呼叫的委派方法,這裡用匿名委派夾帶變數 200 和 300,剛建構出來時還不會執行,可先做一些設定。另外 task2 在 `new` 的時候要寫好 `Task<string>`,不像前面靜態方法 `Task.Run` 可以省略(可以寫 `Task.Run<string>(() => ShowPrime2(300));` 但 IDE 會提示可省略)。 * (3 列、4 列) 給物件 task1 和 task2 做 `.Start()` 才會開始執行。 ## 物件方法 Wait 其實 `Task` 物件的 `.Wait()` 方法前面用很多次了,因為靜態方法 `Task.WhenAll` 就是回傳一個 `Task` 物件,我們都再靠呼叫物件方法 `.Wait()` 來等待。這個方法顧名思義就是會停著,直到該物件執行完,才會往下。下面示範在 ShowPrime2 方法的執行過程中,觀察 `Task` 物件的 task2,其屬性 `IsCompleted` 的布林值: ```csharp= Task<string> task2 = new Task<string>(() => ShowPrime2(300)); Console.WriteLine("開始前的 IsCompleted:" + task2.IsCompleted); task2.Start(); Console.WriteLine("開始後的 IsCompleted:" + task2.IsCompleted); task2.Wait(); Console.WriteLine("等待完的 IsCompleted:" + task2.IsCompleted); if(task2.IsCompleted) { Console.WriteLine("總結:" + task2.Result); } Console.ReadKey(); ``` 結果: ``` 開始前的 IsCompleted:False 開始後的 IsCompleted:False 2 3 5 . . . 1973 1979 1987 等待完的 IsCompleted:True 總結:第300個質數是1987花費81毫秒 ``` 另外物件方法 `.Wait()` 可以放引數 `CancellationToken` 和 `int`,這裡我介紹後者,傳入 `int` 的毫秒數後,`Wait` 方法會回傳 `bool`,表示經過這個毫秒數後 `Task` 物件的是否完成,而且就會往下執行了(一樣的防鎖死用法在靜態方法 `Task.WaitAll` 已有分享)。以下程式碼就是上面的加上 10 毫秒的引數: ```csharp= Task<string> task2 = new Task<string>(() => ShowPrime2(300)); Console.WriteLine("開始前的 IsCompleted:" + task2.IsCompleted); task2.Start(); Console.WriteLine("開始後的 IsCompleted:" + task2.IsCompleted); task2.Wait(10); Console.WriteLine("等待完的 IsCompleted:" + task2.IsCompleted); if (task2.IsCompleted) { Console.WriteLine("總結:" + task2.Result); } Console.ReadKey(); ``` 結果: ``` 開始前的 IsCompleted:False 開始後的 IsCompleted:False 2 3 5 . . . 421 431 433 等待完的 IsCompleted:False 439 443 . . . 1973 1979 1987 ``` 這裡會很清楚看到,「等待完的 IsCompleted:False」是因為經過 10 毫秒其實還沒結束,但程式也往下走了,然後因為還沒結束故沒有用 `.Result` 取 `ShowPrime2` 回傳的字串。 但其實把程式碼第 9 列取 `Result` ,搬到 `if (task2.IsCompleted)` 外面,也就是沒有判斷 `IsCompleted` 的屬性值就直接取用,也會等 `Task` 完整執行完才取,不會拋例外: ```csharp= Task<string> task2 = new Task<string>(() => ShowPrime2(300)); Console.WriteLine("開始前的 IsCompleted:" + task2.IsCompleted); task2.Start(); Console.WriteLine("開始後的 IsCompleted:" + task2.IsCompleted); task2.Wait(10); Console.WriteLine("等待完的 IsCompleted:" + task2.IsCompleted); Console.WriteLine("總結:" + task2.Result); Console.ReadKey(); ``` 結果: ``` 開始前的 IsCompleted:False 開始後的 IsCompleted:False 2 3 5 . . . 421 431 433 等待完的 IsCompleted:False 439 443 . . . 1973 1979 1987 總結:第300個質數是1987花費114毫秒 ``` ## Action 不用匿名委派 用 arrow function 放匿名委派是比較輕鬆的做法,下面是另一個作法: ```csharp= Task task1 = new Task(ShowPrime1UseArgs, 100); Task<string> task2 = new Task<string>(ShowPrime2UseArgs, 200); task1.Start(); task2.Start(); Task.WhenAll(task1, task2).Wait(); Console.WriteLine("ShowPrime2 總結為:" + task2.Result); Console.ReadKey(); void ShowPrime1UseArgs(object? args) { int count = (int)args!; /* ... 省略 ... */ } string ShowPrime2UseArgs(object? args) { int count = (int)args!; /* ... 省略 ... */ } ``` 1 列、2 列放入方法名稱來委派,第二個引數是要傳入的變數,但如同 10 列與 12 列,要修改傳入參數,這種寫法很難寫,但測過效率很好。 ## ContinueWith 既然有完成這件事,就應該有 callback 回呼函式的設計吧?有的,就是 `Task` 物件的 `.ContinueWith()` 方法,傳入委派方法,在 `Task` 完成的時候繼續呼叫,以下示範 task1 回呼用 arrow function 寫的匿名委派方法,印出一行的 `"ShowPrime1 是否完成:"` ;而 task2 回呼已寫好的 ShowResult 方法,傳入 `Task<string>` 物件並用 `.Result` 取其結果: ```csharp= Task task1 = new Task(() => ShowPrime1(200)); task1.ContinueWith(task => Console.WriteLine("ShowPrime1 是否完成:" + task.IsCompleted)); task1.Start(); Task<string> task2 = new Task<string>(() => ShowPrime2(300)); task2.ContinueWith(ShowResult); task2.Start(); Console.ReadKey(); void ShowResult(Task<string> task) { Console.WriteLine("總結為:" + task.Result); } ``` 結果像是: ``` 2 2 3 5 3 7 5 7 . . . 1193 1201 1933 1213 1217 1223 1949 1951 ShowPrime1 是否完成:True 1973 1979 1987 總結為:第300個質數是1987花費197毫秒 ``` ## 同時有 Task 和無 Task 上面的 ShowPrime1 和 ShowPrime2 都是本身內容沒有需要 `await` 其他方法的方法,本身並非 `async` 可等待,用 `Task` 搭配 `Task.WaitAll` 或 `Task.WhenAll` 可在全部都結束時繼續後面的動作。 但是如果多個 Task 其中有方法是可等待,有些不是,要注意寫法,例如以下程式碼 ```csharp= Task task1 = new Task(PrintStar, 20000); Task task2 = new Task(PrintDash, 20000); task1.Start(); task2.Start(); Task.WhenAll(new Task[] { task1, task2 }).Wait(); Console.WriteLine("Done"); Console.ReadKey(); void PrintStar(object? arg) { if (arg == null) return; int amount = (int)arg; for (int i = 0; i < amount; i++) { Console.Write('*'); } } void PrintDash(object? arg) { if (arg == null) return; int amount = (int)arg; for (int i = 0; i < amount; i++) { Console.Write('-'); } } ``` PrintStar 會依傳入數字印出星字號 `*`,PrintDash 會依傳入數字印出減號 `-`,因為以 `Task` 來執行這兩個方法,所以同時交錯印,兩者完成後再印 `Done` 出來。 ``` ******************************---------------- --------*********----***----------*----------- **************----------------*******-------** ------*******-------------**----**------****-- -------------**-------------------****-------- ---**************-------**---*------------**** *****------**********-----*---**-------------- ---***-------------------------Done ``` 如果將 PrintDash 改成去 SQL 撈 Employee 中最大 Salary 來做為印 `-` 的個數,因為有 `await` Dapper 的 `QueryFirstOrDefaultAsync` 方法,所以 PrintDash 方法加了 `async` 且回傳改為 `Task`,如下: ```csharp= Task task1 = new Task(PrintStar, 20000); Task<Task> task2 = new Task<Task>(PrintDash); task1.Start(); task2.Start(); Task.WhenAll(new Task[] { task1, task2 }).Wait(); Console.WriteLine("Done"); Console.ReadKey(); void PrintStar(object? arg) { if (arg == null) return; int amount = (int)arg; for (int i = 0; i < amount; i++) { Console.Write('*'); } } async Task PrintDash(){ string sql = "WAITFOR DELAY '00:00:01'; SELECT TOP(1) Salary FROM [Employee] ORDER BY Salary DESC"; int amount = 0; using (SqlConnection conn = new SqlConnection("...")) { amount = await conn.QueryFirstOrDefaultAsync<int>(sql); } for (int i = 0; i < amount; i++) { Console.Write('-'); } } ``` 以上 task2 用 `Task<Task>` 的寫法會發現 `Done` 字樣,==並不是在 PrintDash 完結後才印出來,前面就印了==,明明程式碼的第 5 列有做 `.Wait()`,結果會像這樣: ``` ********************************************** ********************************************** *************************************--------- ---***********------*******----Done ---------------------------------------------- ---------------------------------------------- ---------------------------------------------- ------------------------ ``` 正確的寫法應該是下面,PrintDash 本身就是可等待會回傳 `Task`,直接用 task2 去接,不需要寫成 `Task<Task>`,而且直接開始了所以 task2 也不需要做 `.Start()` ```csharp= Task task1 = new Task(PrintStar, 20000); Task task2 = PrintDash(); task1.Start(); Task.WhenAll(new Task[] { task1, task2 }).Wait(); Console.WriteLine("Done"); Console.ReadKey(); ``` 正確的結果: ``` ********************************************** ********************************************** *************************************--------- ---***********------*******------------------- ---------------------------------------------- ---------------------------------------------- ---------------------------------------------- --------------------Done ``` 或是如下使用 `Task.Run`,但是此靜態方法無法傳入引數,只能用箭頭函數 ```csharp= Task task1 = new Task(PrintStar, 20000); Task task2 = Task.Run(() => PrintDash()); task1.Start(); Task.WhenAll(new Task[] { task1, task2 }).Wait(); Console.WriteLine("Done"); Console.ReadKey(); ``` 如果用印出 `Thread.CurrentThread.ManagedThreadId` 觀察會發現,使用 `Task task2 = PrintDash();` 的裡面 await 前和外面會是同一個 MangedThreadId,而裡面 await 後和外面是不同 ManagedThreadId。 而使用 `Task task2 = Task.Run(() => PrintDash());`,的裡面外面就是不同 MangedThreadId 了。 ## Queue 範例 利用 `Task` 與 `ContinueWith` 可做一連串的排序作業。如下,PrintStar 方法有兩個參數,第一個 `int` 是印星字號 `*` 的個數,第二個 `int` 是顯示第幾次的呼叫。而 QueuedTasks 是一個 `List` 來存放 `Task` 物件,依序塞入五個 `Task`,再呼叫 ProcessQueue 方法,這個方法中會將 QueuedTasks 最前面的 `Task` 取出來,設定 completed 後會回呼自己,然後有做 `Thread.Sleep(1000);` 在執行完會暫停一秒,也因為要給 `ContinueWith` 做 callback,所以 ProcessQueue 方法要有 `Task` 參數,也就在第一次單獨呼叫時傳入 `null`,而 ProcessQueue 中結束後要回傳一個 `Task.CompletedTask` 作為 `void` 在 `Task` 化後的「已完成工作」。以下參考: ```csharp= IList<Task> QueuedTasks = new List<Task>(); QueuedTasks.Add(new Task(() => PrintStar(10000, 1))); QueuedTasks.Add(new Task(() => PrintStar(10000, 2))); QueuedTasks.Add(new Task(() => PrintStar(10000, 3))); QueuedTasks.Add(new Task(() => PrintStar(10000, 4))); QueuedTasks.Add(new Task(() => PrintStar(10000, 5))); ProcessQueue(null); Console.ReadKey(); Task ProcessQueue(Task task) { if (QueuedTasks.Count > 0) { Task task0 = QueuedTasks[0]; QueuedTasks.RemoveAt(0); task0.ContinueWith(ProcessQueue); task0.Start(); Thread.Sleep(1000); } return Task.CompletedTask; } void PrintStar(int amount, int step) { Console.WriteLine($"這是第{step}次,開始"); for (int i = 0; i < amount; i++) { Console.Write('*'); } Console.WriteLine($"這是第{step}次,結束"); } ``` 上面的 `List` QueuedTasks 的好處是一直 `Add` 要做的 `Task`,只要執行一次 ProcessQueue 方法,就會持續到做完。但是如果同時有多人在執行,例如 API 讓多人同時使用,做諸如 Log 的動作,在背景累積大量的 `Task`,也只能由一個人一個執行緒去執行 ProcessQueue,否則一個已經 `.Start()` 的 `Task` 若再由其他執緒來 `.Start()`,是會拋出例外的。 以上可以加入一個 `bool` 的變數 `IsQueueProcessing` 來控制,呼叫 AddPrintStar 時就先檢查,若 `true` 表示已在執行中則只有做加入 `List`,若 `false` 表示未在執行中就啟動 ProcessQueue。 ```csharp= bool IsQueueProcessing = false; IList<Task> QueuedTasks = new List<Task>(); AddPrintStar(10000, 1); AddPrintStar(10000, 2); AddPrintStar(10000, 3); AddPrintStar(10000, 4); AddPrintStar(10000, 5); Console.ReadKey(); void AddPrintStar(int amount, int step) { QueuedTasks.Add(new Task(() => PrintStar(amount, step))); if (!IsQueueProcessing) { IsQueueProcessing = true; ProcessQueue(null); } } Task ProcessQueue(Task task) { if (QueuedTasks.Count == 0) { IsQueueProcessing = false; } else { Task task0 = QueuedTasks[0]; QueuedTasks.RemoveAt(0); task0.ContinueWith(ProcessQueue); task0.Start(); Thread.Sleep(1000); } return Task.CompletedTask; } void PrintStar(int amount, int step) { Console.WriteLine($"這是第{step}次,開始"); for (int i = 0; i < amount; i++) { Console.Write('*'); } Console.WriteLine($"這是第{step}次,結束"); } ```