以下都以 Dapper 查詢資料庫為例,資料庫有兩個資料表,一個為 Products 有文字的欄位 Name,另一個為 Employees 有數字的欄位 Salary。
using Dapper;
後,SqlConnection
連線物件可用的方法中,早期查詢用 Query
或 Query<T>
,傳回的是多筆 IEnumerable<T>
,後面接 .ToList()
來運用,這是 同步 的方法,也就是一列一列程式碼往下走,還沒得到回應就會停在那裡,直到做完才會往後繼續。
下面程式碼的方法 GetProductName,會撈出 Products 資料表的所有 Name 資料,用頓號 、
串起來傳回來,我們再把文字再顯示在螢幕上,而 WAITFOR DELAY '00:00:03';
是用來模擬網路延遲,可觀察螢幕出現「開始找商品名稱」後的等待時間,會明顯等最少三秒,取得結果才會印出串起來的文字。
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>>
,剛開始可能會被這個回傳的東西嚇到,但看完筆記再回來看這段,就會覺得沒什麼了。先看程式碼:
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);
}
變更的點有:
await
修飾,表示要等待這個查詢動作。.ToList()
但要用括號把整個 await conn.QueryAsync<string>(sql)
包起來再 .ToList()
。async
宣告非同步「可等待」的修飾詞。string
改成 Task<string>
,可想成「回傳一個工作 Task,這個工作夾帶著 string」await
,表示等待這個 Task 完成,取得 string。結果和前面的同步方法是完全一樣的,也是顯示「開始找商品名稱」後停滯三秒後,用頓號列出商品名稱。
上面的兩個範例看不出使用 Task 的好處,我們換個做法,實作上常常要帶回很多資料,只回傳單一的類型不敷使用。所以我會用一個物件來攜帶要取回的資料,然後回傳用 void
。這裡要傳遞用的物件我叫 Carry:
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>
同步方法。
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
就好,以下程式碼:
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;
}
變更點:
QueryAsync
後,前面加上 await
,整個包起來再 .ToList()
。async
宣告可等待,回傳從 void
改成 Task
(其實維持 void
還是可編譯成功,但就沒有可等待效果)。await
等待工作結束。這結果與上面同步結果一樣,查詢商品名稱花費三秒,查詢職員薪水花了兩秒,全部要花五秒才看到最後結果。
試著將上面的 7 列和 10 列維持原來直接呼叫的寫法,不加上 await
修飾:
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
方法了。
在用這個靜態方法之前,要先知道 GetProductName 與 GetEmployeeSalary 方法加上 async
時,回傳的是 Task
,所以可以宣告一個 Task 物件來接 GetProductName 的回傳,可以先看這個物件有什麼內容。
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 查詢還是有微量的時間花費。
如果是最早有回傳 string
的寫法,加上 async
後則是回傳 Task<string>
,
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 方法改成最早的回傳字串版本,相對應的調整如下:
async
宣告可等待,所以回傳為 Task<string>
Task<string>
的 taskP
來接 GetProductName 的工作.Result
來取得 Task<T>
的 T
,在這裡是取得回傳的 string
結果如果有多個 Task ,用靜態方法 Task.WaitAll
可等待這些工作都完成了,才會繼續往下做,他的引數是陣列的 Task,所以是 Task[]
,可以要呼叫使用的時候傳入 new Task[] { task1, task2, task3 }
,也可以先用一個 List<Task>
分別 .Add
最後再 .ToArray
傳入,下面示範用 new Task[]
的做法:
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;
}
string
的做法,所以使用 async
後回傳物件用 Task<string>
。Carry
物件來取得結果,所以使用 async
後回傳物件用 Task
。Task<string>
的 taskP 去接 GetProductName 方法的回傳工作,用 Task
的 taskE 接 GetEmployeeSalary 方法的回傳工作。Task.WaitAll
方法,等待 taskP 和 taskE 兩個 Task
都完成,才會往下繼續 14、15 列,所以 14 列的 .Result
保證會有回傳的商品字串,15 列的 .Employees
List 才不會是空的。但是執行流程一直停在 Task.WaitAll
會有永遠離不開的風險,如果其中一個 Task 出了什麼差錯就全部都在等他了,為預防這種可能,可以加入第二個引數為 int
的毫秒數,當等待了這個毫秒數後若有 Task 還沒有完成,則會回傳 false
,如果全部完成了則回傳 true
,可以試著改寫以上,成為:
if(Task.WaitAll(new Task[] { taskP, taskE }, 2500))
{
Console.WriteLine("工作都完成了");
}
else
{
Console.WriteLine("有工作未完成");
}
因為查詢商品名稱花費三秒,所以等待 2500 毫秒也就是 2.5 秒,只有查詢薪水的工作完成,就會印出「有工作未完成」,可以給 Task.WaitAll
加一個防逾時的毫秒數,當期限到了還有未完成工作,可以再控制程式的流程,例如 throw
例外等等後續處理。
上述的 Task.WaitAll
方法沒有回傳,加了等待毫秒數會有 bool
的回傳,比較難控制,還有另一個靜態方法 Task.WhenAll
,會回傳一個 Task
代表傳入的 Task[]
工作陣列的整體執行狀態,而這個 Task
物件有一個方法 .Wait()
可以等待,以下示範程式碼:
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()
,下面會介紹。
另外還有 Any 版本的靜態方法 Task.WaitAny
和 Task.WhenAny
,等待一堆工作當其中一個完成就繼續,但我沒有實作的機會,也一時沒有想到明瞭的範例,所以就沒有這個的筆記了,但如果要使用應該用法也差不多。
其實 Task.WhenAll
就像 JavaScript 的 Promise.all
,而 Task.WhenAny
就像 Promise.race
。
上面所提的是從 Dapper 的資料庫查詢,如 QueryAsync
或 ExecuteAsync
等非同步方法,延伸到討論 Task 如何操作,但其實不是一定要有 await 某個 async 方法才能使用 Task,例如下面兩個方法 ShowPrime1 和 ShowPrime2 傳入 int
n 會印出前 n 個質數,而 ShowPrimes2 會回傳一個總結字串:
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
物件,以多執行緒來執行引數的委派方法,將以上的直接呼叫,改成:
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 的回傳字串:
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毫秒
上面靜態方法 Task.Run
是馬上就開始執行,若沒有要馬上開始,可以用 new
一個 Task
物件出來,在適當時機用 .Start()
來開始執行,示範如下:
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();
new Task()
建構子傳入的就是要呼叫的委派方法,這裡用匿名委派夾帶變數 200 和 300,剛建構出來時還不會執行,可先做一些設定。另外 task2 在 new
的時候要寫好 Task<string>
,不像前面靜態方法 Task.Run
可以省略(可以寫 Task.Run<string>(() => ShowPrime2(300));
但 IDE 會提示可省略)。.Start()
才會開始執行。其實 Task
物件的 .Wait()
方法前面用很多次了,因為靜態方法 Task.WhenAll
就是回傳一個 Task
物件,我們都再靠呼叫物件方法 .Wait()
來等待。這個方法顧名思義就是會停著,直到該物件執行完,才會往下。下面示範在 ShowPrime2 方法的執行過程中,觀察 Task
物件的 task2,其屬性 IsCompleted
的布林值:
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 毫秒的引數:
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
完整執行完才取,不會拋例外:
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毫秒
用 arrow function 放匿名委派是比較輕鬆的做法,下面是另一個作法:
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 列,要修改傳入參數,這種寫法很難寫,但測過效率很好。
既然有完成這件事,就應該有 callback 回呼函式的設計吧?有的,就是 Task
物件的 .ContinueWith()
方法,傳入委派方法,在 Task
完成的時候繼續呼叫,以下示範 task1 回呼用 arrow function 寫的匿名委派方法,印出一行的 "ShowPrime1 是否完成:"
;而 task2 回呼已寫好的 ShowResult 方法,傳入 Task<string>
物件並用 .Result
取其結果:
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毫秒
上面的 ShowPrime1 和 ShowPrime2 都是本身內容沒有需要 await
其他方法的方法,本身並非 async
可等待,用 Task
搭配 Task.WaitAll
或 Task.WhenAll
可在全部都結束時繼續後面的動作。
但是如果多個 Task 其中有方法是可等待,有些不是,要注意寫法,例如以下程式碼
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
,如下:
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()
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
,但是此靜態方法無法傳入引數,只能用箭頭函數
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 了。
利用 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
化後的「已完成工作」。以下參考:
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。
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}次,結束");
}