C# Task 十分钟轻松学同步非同步

简介与内容概述

预备知识 (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();

范例输出 :

https://ithelp.ithome.com.tw/upload/images/20210711/20131164dOKcY8kTnk.png

https://ithelp.ithome.com.tw/upload/images/20210711/20131164TWzaDXDNKo.png

结果不同的原因, 下方的沟通只定义了等到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 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");
}

<<:  CMoney软件工程师战斗营_心得感想_Week 20

>>:  求 室内设计 vr 视频设计师

Re: 新手让网页 act 起来: Day24 - React Hooks 之 useMemo

前言 昨天我们介绍过如何使用 React.memo 与 useCallback 来做效能优化,而 u...

Material UI in React [ Day 28 ] Customization Component 自订组件 (part1)

由於组件可以在不同的context中使用,有几种方法可以解决这个问题,官方连结。 1.一次性情况的特...

20.unity换场景

今天要盖出阿嬷家!让小红帽走进阿嬷家,找到阿嬷。 1.新建场景 右键 > Create >...

[Day03] Flutter GetX equatable

目前先和大家介绍一些基本的应用 大概Day15~16後会开始结合GetX 一般比较两个 基本型别是否...