简介与内容概述
预备知识 (multi-thread)
在探讨同步非同步之前首先要了解何为thread, 以下内容抄录自维基百科。
执行绪(英语:thread)是作业系统能够进行运算排程的最小单位。大部分情况下,它被包含在行程之中,是行程中的实际运作单位。一条执行绪指的是行程中一个单一顺序的控制流,一个行程中可以并行多个执行绪,每条执行绪并列执行不同的任务。在Unix System V及SunOS中也被称为轻量行程(lightweight processes),但轻量行程更多指核心执行绪(kernel thread),而把使用者执行绪(user thread)称为执行绪。
看起来好像粉複杂, 但其实我们可以简单把其理解为, 一条执行绪就是一系列做事的行程。
所以当我们定义一支程式的执行绪有两条, 想要完成的任务是让使用者可以线上与人进行格斗比赛, 则在概念上可以设计如下两条thread
监听远端伺服器指令来得知敌方操作, 接着同步本地软体内敌方脚色的行为 , 使本地玩家得知对方的行动。监听本地玩家的操作, 接着同步本地软体内我方脚色的行为, 最后上传本地脚色的操作到远端伺服器, 使敌方玩家可以知道我方脚色的操作。我们可以很明显的发现, 若我们的程式是先执行第一件事再执行第二件事, 再回头执行第一件事, 再做第二件事, 即是以如下写法
void enemyHit(){//监听远端伺服器指令来得知敌方操作, 接着同步本地软体内敌方脚色的行为 , 使本地玩家得知对方的行动}void weHit(){//监听本地玩家的操作, 接着同步本地软体内我方脚色的行为, 最后上传本地脚色的操作到远端伺服器, 使敌方玩家可以知道我方脚色的操作。}while(1){enemyHit();weHit()}
就会发生整场格斗都是你一拳我一拳, 我一拳你一拳的回合制战斗.
那该怎么做才可以让玩家来场酣畅淋漓的实时战斗呢? 问题的答案很简单
只要 : 两条流程同时做就可以啦 , 所以程式同时在接收敌方的操作, 也在上传己方的操作,这时我们就可以说这支程式是个双线程(thread)的程式。
注 : 以上内容不包含完整知识, 为了简化概念捨弃了许多内容, 不过用来理解下方教学已经够用了。
简介
所谓同步非同步语法, 即为一种方式可以定义不同条流程分别要做甚么事情, 并且设定两条流程的沟通规则, 包含两条流程谁要先完成谁要后完成, 还是同时做(实务上因为CPU一次只能做一件事, 所以同时做会是以快速的交替做来完成), 都可以在这边定义。
本篇内容
以下会分成4阶段,
第一段说明Task最传统的用法, 如何创建一条新流程, 且主流程, 子流程彼此等待、沟通。第二段说明如何用JS也在用的async/await 语法来取代第一段所完成的程式。这里要注意的是第一段的做法可以被第二段的作法取代, 第二段的做法也可以被第一段的作法取代, 两者都是为了定义不同条流程分别要做甚么事情, 和设定两条流程的沟通规则, 只差在实现的语法不同还有底层的实作原理不同第三段为实战演练, 会利用实际代码给大家看, 这个技巧在生产上可以完成甚么任务。第四段为该概念的进阶用法, 跟前两段没太大关连主要是教大家如何同时调用大量线程(上方所说的流程)传统语法 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的方法为创建一个新线程来寄送, 所以须利用本篇教学的内容来完成撰写。
传统语法
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; } }}
async await
using System; using System.IO; using System.Net.Http; using System.Threading.Tasks; namespace AsyncAwait { 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型别。
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");}