C# Task 十分鐘輕鬆學同步非同步

更新於 發佈於 閱讀時間約 23 分鐘

簡介與內容概述


預備知識 (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();

範例輸出 :

raw-image
raw-image

結果不同的原因, 下方的溝通只定義了等到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");
}
留言
avatar-img
留言分享你的想法!
avatar-img
Leon Lin的沙龍
2會員
1內容數
你可能也想看
Thumbnail
創作者營運專員/經理(Operations Specialist/Manager)將負責對平台成長及收入至關重要的 Partnership 夥伴創作者開發及營運。你將發揮對知識與內容變現、影響力變現的精準判斷力,找到你心中的潛力新星或有聲量的中大型創作者加入 vocus。
Thumbnail
創作者營運專員/經理(Operations Specialist/Manager)將負責對平台成長及收入至關重要的 Partnership 夥伴創作者開發及營運。你將發揮對知識與內容變現、影響力變現的精準判斷力,找到你心中的潛力新星或有聲量的中大型創作者加入 vocus。
Thumbnail
當你需要在 Python 中執行多個任務,但又不希望它們相互阻塞時,可以使用 threading 模組。 threading 模組允許你在單個程序中創建多個執行緒,這些執行緒可以同時運行,從而實現並行執行多個任務的效果。
Thumbnail
當你需要在 Python 中執行多個任務,但又不希望它們相互阻塞時,可以使用 threading 模組。 threading 模組允許你在單個程序中創建多個執行緒,這些執行緒可以同時運行,從而實現並行執行多個任務的效果。
Thumbnail
  當我們要使用執行緒的時候,就要引用System.Threading的類別庫,程式才可以使用唷!這篇內容先說明:建立與啟用執行緒、跨執行緒控制UI介面、等待或暫停時間執行緒、共享資源的部分。
Thumbnail
  當我們要使用執行緒的時候,就要引用System.Threading的類別庫,程式才可以使用唷!這篇內容先說明:建立與啟用執行緒、跨執行緒控制UI介面、等待或暫停時間執行緒、共享資源的部分。
Thumbnail
  在說執行緒(Thread)時就要先知道什麼是程式(Program)、程序(Process),才能了解什麼是執行緒(Thread),因為它們間都有著神秘的關係與關聯,再更深入一點就又會有多程序(muti-Process)、多執行緒(muti-Thread),我們就先一一說明好了,了解它們後再使用時
Thumbnail
  在說執行緒(Thread)時就要先知道什麼是程式(Program)、程序(Process),才能了解什麼是執行緒(Thread),因為它們間都有著神秘的關係與關聯,再更深入一點就又會有多程序(muti-Process)、多執行緒(muti-Thread),我們就先一一說明好了,了解它們後再使用時
Thumbnail
Web Workers主要提供簡單的API讓網頁在背景執行緒中執行程式而不干擾使用者的操作。 javascript主要功能是與user操作頁面互動及操作dom,試想若使用多執行緒的概念,那麼一個動作是新增至某個dom節點,另一個動作則是修改該dom節點,此時瀏覽器應該使用哪個動作為準? 所以為了避免
Thumbnail
Web Workers主要提供簡單的API讓網頁在背景執行緒中執行程式而不干擾使用者的操作。 javascript主要功能是與user操作頁面互動及操作dom,試想若使用多執行緒的概念,那麼一個動作是新增至某個dom節點,另一個動作則是修改該dom節點,此時瀏覽器應該使用哪個動作為準? 所以為了避免
Thumbnail
介紹 委派的非同步方法 可以透過BeginInvoke執行委派的非同步方法 Action<T>.BeginInvoke(<T> obj,AsyncCallback callback,Object @object) 第一個內容的 obj,只的是要傳入acction委派的參數 第二個AsyncCallb
Thumbnail
介紹 委派的非同步方法 可以透過BeginInvoke執行委派的非同步方法 Action<T>.BeginInvoke(<T> obj,AsyncCallback callback,Object @object) 第一個內容的 obj,只的是要傳入acction委派的參數 第二個AsyncCallb
Thumbnail
介紹 📷 定義 處理序(Process) (大陸:進程): 一個程序運行時,占用全部計算資源的總和 執行緒(Thread) (大陸:線程):是作業系統能夠進行運算排程的最小單位。 大部分情況下,它被包含在行程之中,是行程中的實際運作單位。 C#多線程和異步(一)——基本概念和使用方法 執行緒帶來的
Thumbnail
介紹 📷 定義 處理序(Process) (大陸:進程): 一個程序運行時,占用全部計算資源的總和 執行緒(Thread) (大陸:線程):是作業系統能夠進行運算排程的最小單位。 大部分情況下,它被包含在行程之中,是行程中的實際運作單位。 C#多線程和異步(一)——基本概念和使用方法 執行緒帶來的
Thumbnail
介紹 計算機架構相關名詞 📷 中央處理器 (CPU)Central Processing Unit Processors (處理器) Sockets (實體插槽) Cores (實體核心) Logical processors (邏輯核心) 超執行緒(HT)Hyper-Threading Tec
Thumbnail
介紹 計算機架構相關名詞 📷 中央處理器 (CPU)Central Processing Unit Processors (處理器) Sockets (實體插槽) Cores (實體核心) Logical processors (邏輯核心) 超執行緒(HT)Hyper-Threading Tec
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News