所謂**同步非同步語法**, 即為一種方式**可以定義不同條流程分別要做甚麼事情**, 並且**設定兩條流程的溝通規則,** 包含兩條流程誰要先完成誰要後完成, 還是同時做(實務上因為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的功能是同時執行其傳入作為參數的所有線程, 且傳入參數須為ListTask型別。
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");
}