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();
範例輸出 :
結果不同的原因, 下方的溝通只定義了等到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");
}
2會員
1內容數
留言0
查看全部
發表第一個留言支持創作者!
你可能也想看
C 台灣的新創團隊 如果想要使用以太坊技術 來營利同時做公益慈善 可以發展那些專案C 台灣的新創團隊 如果想要使用以太坊技術 來營利同時做公益慈善 可以發展那些專案 #VitalikButerin #ETHTaipei2024 #ETHTaipeiHackathon2024 ... (好的顧問導師教練 協助妳提早得到幸福 更快實現夢想 幸福課程
avatar
leader
2024-06-17
C#5 台灣的藥局:勝過7-11的零售業的新秀 截至2022年,全台的藥局數量已達到10584家;而同年,便利商店的數量大約有13000家,其中7-11(小七)共有6631家。換句話說,藥局的店數已經超過了小七。為什麼藥局的店數能夠超越 7-11 呢?
Thumbnail
avatar
茉莉
2024-05-27
C#入門-Day10:套件本章講述了C#開發中的程序集,命名空間和 NuGet 包管理器。程序集是 .NET 應用的基礎,命名空間用於組織和預防命名衝突,而 NuGet 用於管理 .NET 的外部庫和依賴項。
Thumbnail
avatar
浴火重生的雞
2024-05-25
C#入門-Day9:例外處理本章節介紹C#的「例外處理」,包括使用try-catch語法處理錯誤,finally關鍵字的使用,以及如何主動引發和自定義異常。
Thumbnail
avatar
浴火重生的雞
2024-05-25
C#入門-Day8:物件導向本章節的目的是讓讀者瞭解C#的物件導向特性,包括類別、繼承、多型、封裝等基本概念,以及介面、抽象類別、靜態類別等進階主題。此外,本章節也將介紹如何使用列舉、委派、Lambda表達式、泛型及反射,這些都是C#中常見的強大功能。
Thumbnail
avatar
浴火重生的雞
2024-05-25
雲頂12年桶裝#14威士忌Springbank 12YO Cask Strength Whisky Batch 14比#13好,打火石感重,比較乾澀,橘子果醬 購入地點:橡木桶
Thumbnail
avatar
盧克開酒評
2022-10-27
雲頂12年桶裝#13威士忌Springbank 12YO Cask Strength Whisky Batch 13與#12相較比較沒有椰子味,較烈,堅果,杏仁 購入地點:橡木桶
Thumbnail
avatar
盧克開酒評
2022-10-27
雲頂12年桶裝#12威士忌Springbank 12YO Cask Strength Whisky Batch 12 燻肉,果皮,椰子糖,溫和,麥香 口感胡椒,煮水果,水果軟糖,橘皮果醬 購入地點:橡木桶
Thumbnail
avatar
盧克開酒評
2022-10-27
愛倫Bothy#2威士忌Arran the Bothy#2 Quarter Cask Scotch Whisky柑橘,蘋果,巧克力蛋糕,布丁,一點草本,一點泥煤,口感烈,花香,中木材味,帶點霉味 購入地點:橡木桶
Thumbnail
avatar
盧克開酒評
2022-10-24
百富25年威士忌The Balvenie 25YO Triple Cask Single Malt Whisky英國百富25年單一麥芽蘇格蘭威士忌The Balvenie 25YO Triple Cask Single Malt Scotch Whisky 很討喜的口感,有點椰子,橘皮香,雪梨果香
Thumbnail
avatar
盧克開酒評
2022-10-21