簡介與內容概述
預備知識 (multi-thread)
在探討同步非同步之前首先要了解何為thread, 以下內容抄錄自維基百科。
執行緒(英語:thread)是作業系統能夠進行運算排程的最小單位。大部分情況下,它被包含在行程之中,是行程中的實際運作單位。一條執行緒指的是行程中一個單一順序的控制流,一個行程中可以並行多個執行緒,每條執行緒並列執行不同的任務。在Unix System V及SunOS中也被稱為輕量行程(lightweight processes),但輕量行程更多指核心執行緒(kernel thread),而把使用者執行緒(user thread)稱為執行緒。
看起來好像粉複雜, 但其實我們可以簡單把其理解為, 一條執行緒就是**一系列做事的行程**。
所以當我們定義一支程式的執行緒有兩條, 想要完成的任務是讓使用者可以線上與人進行格鬥比賽, 則在概念上可以設計如下兩條thread
1. 監聽遠端伺服器指令來得知敵方操作, 接著同步本地軟體內敵方腳色的行為 , 使本地玩家得知對方的行動。
2. 監聽本地玩家的操作, 接著同步本地軟體內我方腳色的行為, 最後上傳本地腳色的操作到遠端伺服器, 使敵方玩家可以知道我方腳色的操作。
我們可以很明顯的發現, 若我們的程式是先執行第一件事再執行第二件事, 再回頭執行第一件事, 再做第二件事, 即是以如下寫法
void enemyHit(){
//監聽遠端伺服器指令來得知敵方操作, 接著同步本地軟體內敵方腳色的行為 , 使本地玩家得知對方的行動
}
void weHit(){
//監聽本地玩家的操作, 接著同步本地軟體內我方腳色的行為, 最後上傳本地腳色的操作到遠端伺服器, 使敵方玩家可以知道我方腳色的操作。
}
while(1){
enemyHit();
weHit()
}
就會發生整場格鬥都是你一拳我一拳, 我一拳你一拳的回合制戰鬥.
那該怎麼做才可以讓玩家來場酣暢淋漓的實時戰鬥呢? 問題的答案很簡單
只要 : ***兩條流程同時做就可以啦 , 所以程式同時在接收敵方的操作, 也在上傳己方的操作,***這時我們就可以說這支程式是個雙線程(thread)的程式。
注 : 以上內容不包含完整知識, 為了簡化概念捨棄了許多內容, 不過用來理解下方教學已經夠用了。
簡介
所謂**同步非同步語法**, 即為一種方式**可以定義不同條流程分別要做甚麼事情**, 並且**設定兩條流程的溝通規則,** 包含兩條流程誰要先完成誰要後完成, 還是同時做(實務上因為CPU一次只能做一件事, 所以同時做會是以快速的交替做來完成), 都可以在這邊定義。
本篇內容
以下會分成4階段,
1. 第一段說明Task最傳統的用法, 如何創建一條新流程, 且主流程, 子流程彼此等待、溝通。
2. 第二段說明如何用JS也在用的async/await 語法來取代第一段所完成的程式。這裡要注意的是第一段的做法可以被第二段的作法取代, 第二段的做法也可以被第一段的作法取代, 兩者都是為了**定義不同條流程分別要做甚麼事情**, 和**設定兩條流程的溝通規則,** 只差在實現的語法不同
3. 第三段為實戰演練, 會利用實際代碼給大家看, 這個技巧在生產上可以完成甚麼任務。
4. 第四段為該概念的進階用法, 跟前兩段沒太大關連主要是教大家如何同時調用大量線程(上方所說的流程)
傳統語法 Awaiter
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
//創建4條子線程
Task subThread1 = new Task(() =>
{
//這裡可以填入一系列要讓該線程做的事
Thread.Sleep(1000);
Console.WriteLine("I am subThread1!");
});
Task subThread2 = new Task(() =>
{
//這裡可以填入一系列要讓該線程做的事
Thread.Sleep(1000);
Console.WriteLine("I am subThread2!");
});
Task subThread3 = new Task(() =>
{
//這裡可以填入一系列要讓該線程做的事
Thread.Sleep(1000);
Console.WriteLine("I am subThread3!");
});
Task subThread4 = new Task(() =>
{
//這裡可以填入一系列要讓該線程做的事
Thread.Sleep(1000);
Console.WriteLine("I am subThread4!");
});
// 讓線程溝通
// 讓2條線程開始跑
subThread2.Start();
subThread1.Start();
// GetAwaiter() : 等待完成, OnCompleted() : 線程完成後要做的事
subThread1.GetAwaiter().OnCompleted(()=> {
// 線程完成後要做的事
// 讓2條線程開始跑, 當第一條線程跑完
subThread4.Start();
subThread3.Start();
});
// GetAwaiter() : 等待完成, GetResult() : 取得結果
// 實際寫法範例 result = subThread2.GetAwaiter().GetResult(); (這裡只是因為沒有回傳才這樣寫)
subThread2.GetAwaiter().GetResult();
// 等待線程完成才繼續往下走
subThread3.Wait();
subThread4.Wait();
}
}
}
若是實際運行上述代碼於本地會發現每次結果相異, 以下解釋相關重點資訊。
// 此處定義了如何定義新線程(一條獨立的做事流程), 但其並沒有開始運行
Task subThread1 = new Task(() =>
{
Thread.Sleep(1000);
Console.WriteLine("I am subThread1!");
});
// 下達指令, 創建線程開始運行, 所以此刻程式包含該程式的主線程, 總共有三條獨立的做事流程在進行。
subThread2.Start();
subThread1.Start();
// subThread1線程完成後創建subThread3 subThread4線程開始運行
subThread1.GetAwaiter().OnCompleted(()=> {
subThread4.Start();
subThread3.Start();
});
// 等待 subThread3完成主線程才繼續往下
subThread3.Wait();
範例輸出 :
結果不同的原因, 下方的溝通只定義了等到XXX完成才繼續, 但在XXX.Start()後, 一堆線程早就同時在運行了, 有可能主線程還沒運行到等待那行, XXX就已經完成了。
常見用法 async await
與第一部分程式碼效果基本一致, 可以自行對照
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
//Main 為C#進入點, 不可為非同步函式, 所以用傳統語法對我們的同步函數進行包裝
int n = main().GetAwaiter().GetResult();
Console.WriteLine(n);
}
// 同步函數會回傳、創建且運行一個線程, return後方的回傳值會直接包在Task裡面
// 所以若是 return後面是一個字串, 則實際傳出的就是一個 Task<string>
static async Task createTask(int threadNum)
{
//這裡可以填入一系列要讓該線程做的事
// await表示該線程完成再繼續執行, Task.Delay表示創建一個線程其行為為等待XXX毫秒
await Task.Delay(1000);
Console.WriteLine($"I am subThread{threadNum}!");
return;
}
static async Task<int> main()
{
//創建且執行兩條新線程
Task subThread1 = createTask(1);
Task subThread2 = createTask(2);
// 等待某一線程完成
await subThread1;
//創建且執行兩條新線程
Task subThread3 = createTask(3);
Task subThread4 = createTask(4);
// 等待以下線程完成後 return
await subThread2;
await subThread3;
await subThread4;
return 1;
}
}
}
實戰演練
以下為call API常用到的程式碼, 引用組件, 寄送http request, 由於該組件寄request的方法為創建一個新線程來寄送, 所以須利用本篇教學的內容來完成撰寫。
1. 傳統語法
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace general
{
class Program
{
static HttpClient client = new HttpClient();
static void Main(string[] args)
{
//讀取參數 非本教學重點
StreamReader r = new StreamReader("xxx.json");
string jsonString = r.ReadToEnd();
string res = PostRequest("API", jsonString);
}
public static string PostRequest(string URI, string PostParams)
{
//設定API 非本教學重點
client.BaseAddress = new Uri("http://XXX");
client.DefaultRequestHeaders.Add("sat", "1234");
client.DefaultRequestHeaders.Add("sid", "1234");
client.DefaultRequestHeaders.Add("code", "");
client.Timeout = TimeSpan.FromSeconds(30);
//創建新線程, 實際利用組件寄request, 且等待http response回傳才會繼續往下走。(該組件該函數會自行創建線程且運行)
HttpResponseMessage response = client.PostAsync(URI, new StringContent(PostParams)).GetAwaiter().GetResult();
//創建新線程, 利用組件解讀response, 且等待解讀完成回傳後才繼續運行。(該組件該函數會自行創建線程且運行)
string content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
return content;
}
}
}
2. async await
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace AsyneAwait
{
class Program
{
static HttpClient client = new HttpClient();
static void Main(string[] args)
{
main().GetAwaiter().GetResult();
}
static async Task main()
{
//讀取參數 非本教學重點
StreamReader r = new StreamReader("xxx.json");
string jsonString = r.ReadToEnd();
//創建新線程來完成寄request, 且等待回傳才會繼續往下走。
string res = await PostRequest("API", jsonString);
//do things about res
return;
}
public static async Task<string> PostRequest(string URI, string PostParams)
{
//設定API 非本教學重點
client.BaseAddress = new Uri("http://XXX");
client.DefaultRequestHeaders.Add("sat", "1234");
client.DefaultRequestHeaders.Add("sid", "1234");
client.DefaultRequestHeaders.Add("code", "");
client.Timeout = TimeSpan.FromSeconds(30);
//創建新線程, 實際利用組件寄request, 且等待http response回傳才會繼續往下走。
HttpResponseMessage response = await client.PostAsync(URI, new StringContent(PostParams));
//創建新線程, 利用組件解讀response, 且等待解讀完成回傳後才繼續運行。
string content = await response.Content.ReadAsStringAsync();
//回傳解讀內容給正在等待的父線程
return content;
}
}
}
進階用法 whenAll
本程式的目的為把pathOfFolder資料夾下從0.jpg~99.jpg的檔案名變更成0_new.jpg~99_new.jpg
其中利用把整個操作打包成一個線程, 再把線程複製100次, 使其可以100件事同時做(實際上基於底層原理不會如此理想)。
whenAll的功能是同時執行其傳入作為參數的所有線程, 且傳入參數須為List<Task>型別。
private void whenAllDemo()
{
List<Task> taskList = new List<Task>();
for(int i = 0; i < 100; i++)
{
string sourceName = i.toString();
string disName = i.toString() + "_new";
taskList.Add(Task.Run(() => {
try
{
File.Move($"{pathOfFolder}\\{sourceName}.jpg" , $"{pathOfFolder}\\{disName}.jpg");
}
catch (Exception err)
{
MessageBox.Show(err.Message);
throw;
}
}));
}
Task allTask = Task.WhenAll(taskList);
try
{
allTask.Wait();
}
catch { }
if (allTask.Status == TaskStatus.RanToCompletion)
MessageBox.Show("success!");
else if (allTask.Status == TaskStatus.Faulted)
MessageBox.Show("something wrong");
}