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

简介与内容概述

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

範例输出 :

http://img2.58codes.com/2024/20131164dOKcY8kTnk.png

http://img2.58codes.com/2024/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的方法为创建一个新线程来寄送, 所以须利用本篇教学的内容来完成撰写。

传统语法

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");}

关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章