【C#】并行编程实战:任务并行性(上)

news2024/11/27 14:46:36

        在 .NET 的初始版本中,我们只能依赖线程(线程可以直接创建或者使用 ThreadPool 类创建)。ThreadPool 类提供了一个托管抽象层,但是开发人员仍然需要依靠 Thread 类来进行更好的控制。而 Thread 类维护困难,且不可托管,给内存和 CPU 带来沉重负担。

        因此,我们需要一种方案,既能充分利用 Thread 类的优点,又规避它的困难。这就是任务 (Task)。

        (另:本章篇幅较大,将分为上种下三部分发表。)


1、任务(Task)的特性

        任务(Task)是 .NET 中的抽象,一个异步单位。从技术上讲,任务不过是对线程的包装,并且这个线程还是通过 ThreadPool 创建的。但是任务提供了诸如等待、取消和继续之类的特性,这些特性可以在任务完成后运行。

        任务具有以下重要特性:

  • 任务由 TaskScheduler (任务调度程序)执行,默认的调度仅在 ThreadPool 上运行。

  • 可以从任务中返回值。

  • 任务在完成时有通知(ThreadPool 和 Thread 都没有)。

  • 可以使用 ContinueWith() 构造连续执行的任务。

  • 可以通过调用 Task.Wait() 等待任务的执行,这将阻塞调用线程,直到任务完成为止。

  • 与传统线程或 ThreadPool 相比,任务可以使代码的可读性更高。他们还为在 C# 5.0 中引入异步编程构造铺平了道路。

  • 当一个任务从另一个任务启动时,可以建立它们之间的父子级关系。

  • 可以将子任务的异常传播到父任务。

  • 可以使用 CancellationToken 类取消任务。

2、创建和启动任务

        我们可以通过多种方式使用任务并行库(TPL)创建和运行任务。

2.1、使用 Task

        Task 类是作为 ThreadPool 线程异步执行工作的一种方式。它采用的是基于任务的异步模式( Task-Based Asynchronous Pattern,TAP)。非通用 Task 类不会返回结果,因此当需要从任务中返回值时,就需要使用通用版本的 Task<T> 。Task 需要调用 Start 方法来调度运行。

        具体的 Task 调用代码如下:

        /// <summary>
        /// 测试方法,打印10次,等待10秒
        /// </summary>
        public static void DebugAndWait()
        {
            int length = 10;
            for (int i = 0; i < length; i++)
            {
                Debug.Log($"执行第:{i + 1}/{length} 次打印!");
                Thread.Sleep(1000);
            }
        }
        
        //使用任务执行
        private void RunByNewTask()
        {
            //创建任务
            Task task = new Task(TestFunction.DebugAndWait);
            task.Start();//不调用 Start 则不会执行
        }

        最终结果也没有什么意外:

 

2.2、使用 Task.Factory.StartNew

        TaskFactory 类的 StartNew 方法也可以创建任务。这种方式创建的任务将安排在 ThreadPool 中执行,然后返回该任务的引用:

        private void RunByTaskFactory()
        {
            //使用 Task.Factory 创建任务,不需要调用 Start
            var task = Task.Factory.StartNew(TestFunction.DebugAndWait);
        }

        当然打印的结果和上述一样的。

2.3、使用 Task.Run

        这个原理和 Task.Factory.StartNew 一样:

        private void RunByTaskRun()
        {
            //使用 Task.Run 创建任务,不需要调用 Start
            var task = Task.Run(TestFunction.DebugAndWait);
        }

2.4、Task.Delay

        使用 Task.Delay 也可以创建一个任务,但是这个任务有点特别。它可以在指定时间间隔后完成,可以使用

        CacellationToken 类随时取消。与 Thread.Sleep 不同,Task.Delay 不需要利用 CPU 周期,且可以异步运行。

        为了体现两者的不同,我们直接写个例子:

        public static void DebugWithTaskDelay()
        {
            Debug.Log("TaskDelay Start");
            Task.Delay(2000);//等待2s        
            Debug.Log("TaskDelay End");
        }

        然后我们直接在程序中直接同步调用此方法:

        private void RunWithTaskDelay()
        {
            Debug.Log("开始测试 Task.Delay !");
            TestFunction.DebugWithTaskDelay();
            Debug.Log("结束测试 Task.Delay !");
        }

        结果如下:

         可以看到4条打印按照顺序一瞬间被打印出来了,根本没有任何等待。而如果我们把上述的 Task.Delay 替换成 Thread.Sleep,结果会如何呢?

         在运行此方法后,Unity直接卡住,然后2s后打印出4条信息。并且,显然线程等待生效了,但是是以阻塞主线程的方式生效的。

        让我们换回 Task.Delay ,并使用 Task.Run 来运行这个方法,打印结果如下:

         显然线程等待命令生效了,说明在子线程中的 Delay 是可以正常工作的。

2.5、Task.Yield

        Task.Yiled 是创建 await 任务的另一种方法。使用此方法可以让方法强制变成异步的,并将控制权返回给操作系统。

        怎么理解呢?我们这里需要一个很耗时的函数:

        public static async void DebugWithTaskYield()
        {
            int length = 27;//这个方法不能执行很多次
            string str = "";

            for (int i = 0; i < length; i++)
            {
                //以下是耗时函数
                str += "1,1";
                var arr = str.Split(',');
                foreach (var item in arr)
                {
                    str += item;
                }

                await Task.Yield();
                Debug.Log($"执行第:{i + 1}/{length} 次打印!");
            }
        }

        这里我直接用简单的字符串拼接来实现了耗时函数。

        我们在主线程调用 Task.Run 来执行,Debug 的结果如下:

         可以看到随着字符串的增加,单次耗时越来越长。但是无论单次耗时时长有多少,都没有阻碍主线程!可能大家第一感觉和 Unity 的协程是一样的,但是 Unity 的协程使用是在主线程运行的,使用协程并不代表不会阻塞主线程。这里我们直接将这段代码用协程的逻辑实现:

        public static IEnumerator DebugWithCoroutine()
        {
            int length = 27;//这个方法不能执行很多次
            string str = "";

            for (int i = 0; i < length; i++)
            {
                //以下是耗时函数
                str += "1,1";
                var arr = str.Split(',');
                foreach (var item in arr)
                {
                    str += item;
                }

                yield return null;
                Debug.Log($"执行第:{i + 1}/{length} 次打印!");
            }
        }

        逻辑上没有任何区别,就是把 await Task.Yield(); 改成了 yield return null 。当然,日志打印上看起来差不多,但是对主线程而言有本质区别。当运行到后面时,每次迭代都会造成主线程的卡顿。这一点在 Profiler 上看起来非常明显:

 (可以看到协程调用的显然耗时)

2.6、Task.FromResult

        FromResult<T> 是在 .NET Framework 4.5 中才被引入的方法,这在 Unity 2022.2.5 f1c1 使用的 .NET Standard 2.1 是支持的。

        public static int FromResultTest()
        {
            int length = 100;
            int result = 0;
            for (int i = 0; i < length; i++)
                result += Random.Range(0, 100);
            Debug.Log($"FromResultTest 运算结果:{result} ");
            return result;
        }
        
        private void RunWithFromResult()
        {
            Debug.Log("RunWithFromResult Start !");
            Task<int> resultTask = Task.FromResult<int>(TestFunction.FromResultTest());
            Debug.Log("RunWithFromResult End ! Result : " + resultTask.Result);
        }

        如上述代码所示 RunWithFromResult 的结果如下:

         与一般的Task异步不同,这里是按照执行顺序依次打印的。如果这个函数是个耗时函数,会阻塞主线程吗?我把 2.5 里测试的耗时函数搬过来测试了一下(就不贴代码了):

         显然已经阻塞主线程了。

        也就是说这个 FromResult 将异步的方法拿到主线程中调度了(也可以理解为把子线程直接拿到父线程)。既然已经是 Unity 主线程了,那么 Task.Delay 就不会生效;而 Thread.Sleep 会生效,且会阻塞主线程。

        与前面的几个创建Task任务的方法不同,这个Task.FromResult 是可以调用带参函数的(Task.Run 只能运行无参函数)。但即便如此,因为其会阻塞父线程,也不建议在 Unity 主线程中使用。

2.7、Task.FromException 和 Task.FromException<T>

        这两个方法都可以抛出异步任务中的异常,在单元测试中很有用。

        (这里暂时不会用到,就先不讲了,在后面学单元测试的时候再详细学习这两个)

2.8、Task.FromCanceled 和 Task.FromCanceled<T>

                这个和 Task.FromException 的情况有点类似,都是看起来不知道有啥用其实很有用的方法。为了方便学习,这里还是展开讲讲。

        首先看下面一段代码,这个也是 Task.FromCanceled 的示例代码:

CancellationTokenSource source = new CancellationTokenSource();//构建取消令牌源
source.Cancel();//设置为取消

//返回标记为取消的任务。
//注意!使用此方法要确保 CancellationTokenSource 已经调用过 Cancel 方法 ,否则会出错!
Task.FromCanceled(source.Token);

        当我们把这个最后得到的Task状态(Task.Status)打印出来,其结果是便是 Created 。

        肯定就有人会问了,这个有啥用啊?我是创建了一个取消的任务?那我执行这段代码的意义是什么呢?

        单看这段代码,确实没什么意义,但是我们这里提出一个需求:

         逻辑很简单,但是问题就出在最后,要维护一个Task。我们假设预计执行的任务A是某个长期的异步函数,外部需要检测他的状态和结果。那我们在输入偶数的时候,该返回什么呢?首先肯定不能返回一个空的Task,这个返回就和正常的Task一样的了,外部监控的状态要么是 WaitingToRun, 要么就是 RanToCompletion,要么就是 Running 。我根本无法知道我是执行了 任务A 还是没有执行 任务A。

        这时候就发现 Task.FromCanceled 的作用了:

        private void RunWithFromCanceled()
        {
            var val = commonPanel.GetInt32Parameter();
            //这里测试输入双数就取消执行,单数就正常执行。
            CancellationTokenSource source = new CancellationTokenSource();
            if (val % 2 == 0)
                source.Cancel();
            var task = TestFunction.TestCanceledTask(source);
            Debug.Log($"Task State 1: {task.Status}");
        }
        
        /// <summary>
        /// 测试用于取消任务
        /// </summary>
        public static Task TestCanceledTask(CancellationTokenSource source)
        {
            if (source.IsCancellationRequested)
            {
                Debug.Log($"任务取消 !");
                var token = source.Token;       
                return Task.FromCanceled(token);
            }
            else
            {
                Debug.Log($"任务执行 !");
                return Task.Run(DebugWithTaskDelay);
            }
        }

        当输入偶数时,就会返回一个已取消的任务,而奇数则会正常执行。

        当我们对任务进行了封装,内部的判断逻辑会比较复杂,而外部也只需要知道任务执行情况而不需要知道其内部逻辑。此时使用 Task.FromCanceled 和 Task.FromException 就能返回给外部一个通用的“异常”Task。

3、从完成的任务中获取结果

        任务并行库(TPL)中提供的API有如下几个:

        /// <summary>
        /// 获取任务并行结果
        /// </summary>
        private void GetTaskResult()
        {
            int inputParam = commonPanel.GetInt32Parameter();
            Debug.Log($"get task result start ! paramter :  {inputParam}");

            //方法1 :new Task
            var task_1 = new Task<int>(()=>TestFunction.FromResultTest(inputParam));
            task_1.Start();
            Debug.Log($"task_1 result : {task_1.Result}");

            //方法2:Task.Factory
            var task_2 = Task.Factory.StartNew<int>(()=> TestFunction.FromResultTest(inputParam));
            Debug.Log($"task_2 result : {task_2.Result}");

            //方法3:
            var task_3 = Task.Run<int>(()=>TestFunction.FromResultTest(inputParam));
            Debug.Log($"task_3 result : {task_3.Result}");

            //方法4:
            var task_4 = Task.FromResult<int>(TestFunction.FromResultTest(inputParam));
            Debug.Log($"task_4 result : {task_4.Result}");
        }

        这次测试终于出现了一个熟悉的错误:

         Random.Range 只能在Unity主线程使用。

        这个以前就知道 UnityEngine 的类不能在子线程使用,这里遇到了。但是没关系,我们直接修改这个方法即可,用System的Random就行了。

        但是这能说明我们的程序确实在子线程运行了,但是实际上这4个方法都是会阻塞主线程的

         所有的运算流程都是和 2.6 的 FromResult 一样,已经将子线程调回主线程使用了。显然这几种方法都是提供一种同步的结果获取,而真正做到异步计算还不能直接这么使用。


        限于篇幅,任务并行性(上)到此为止。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/646985.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

优惠券超发问题该怎么测试?

在拼夕夕面试中&#xff0c;面试官问了一连串经典的问题&#xff1a;“优惠券库存是怎么扣减的&#xff1f;开发为了解决超发优惠券问题而设计的方案&#xff0c;你了解过吗&#xff1f;你又是如何测试的呢&#xff1f;” 当时听到这些问题还挺懵的&#xff0c;没遇到过超发问…

MidJourney教程02

1.主体内容&#xff1a;高数AI你需要画什么&#xff1f;比如说&#xff0c;一个男生在电脑前画画&#xff1f; 2.环境北京&#xff1a;例如给某些地点或者物件&#xff0c;比如桌子上&#xff0c;足球场&#xff0c;水面有倒影等&#xff1f; 3.构图镜头&#xff1a;比如说强…

springboot项目外卖管理 day07-功能补充

文章目录 前端补充功能1、历史订单功能1.1、梳理过程1.2历史订单展示1.3、效果展示 2、修改/删除地址2.1、回显数据梳理过程 代码展示 2.2、修改地址梳理过程代码 2.3、删除地址梳理过程代码展示 3、再来一单功能3.1、梳理过程3.2、具体实现思路&#xff08;参考一下当初我们怎…

Linux操作系统——第四章 进程间通信

目录 进程间通信介绍 进程间通信目的 进程间通信发展 进程间通信分类 管道 System V IPC POSIX IPC 管道 什么是管道 匿名管道 管道读写规则 管道特点 命名管道 创建一个命名管道 匿名管道与命名管道的区别 命名管道的打开规则 system V共享内存 共享内存示意…

【SpringBoot】解决依赖版本不一致报错问题

哈喽大家好&#xff0c;我是阿Q。今天在开发代码的过程中&#xff0c;由于手抖&#xff0c;不知道引入了什么包依赖&#xff0c;导致项目启动一直报错&#xff0c;特写本文来记录下解决问题的经过。 文章目录 问题描述报错信息如下报错描述 解决方法总结 问题描述 报错信息如下…

vite中使用 vite- aliases 插件报错

vite 中使用 vite-aliases 插件报错 vite-aliases 介绍报错内容解决方法 vite-aliases 介绍 vite-aliases 可以帮助我们自动生成别名: 检测你当前目录下包括 src 在内的所有文件夹, 并帮助我们去生成别名。 下载 npm i vite-aliases -D 使用 import { defineConfig } from vi…

VALSE 2023 无锡线下参会个人总结 6月11日-2

VALSE2023无锡线下参会个人总结 6月11日-2 6月11日会议日程安排Workshop&#xff1a;目标检测与分割程明明&#xff1a;粒度自适应的图像感知技术张兆翔&#xff1a;基于多传感器融合的视觉物体检测与分割 Workshop&#xff1a;ChatGPT与计算机视觉白翔&#xff1a;再谈ChatGPT…

290. 单词规律

290. 单词规律 C代码&#xff1a;别人手搓的 bool wordPattern(char * pattern, char * s){char arr[301][3001];char *p strtok(s, " ");int pos 0;while(p ! NULL) {sprintf(arr[pos], "%s", p);p strtok(NULL, " ");}int len strlen(pat…

Linux环境安装Jdk图文步骤

准备工作&#xff1a; a、jdk安装包&#xff1a;百度网盘 请输入提取码&#xff0c;提取码&#xff1a;jdk8 b、远程工具&#xff0c;xshell&#xff0c;&#xff0c;electerm&#xff0c;&#xff0c;MobaXterm&#xff0c;&#xff0c;fxp&#xff0c;docker&#xff0c;宝…

软件测试V、W和H模型的优缺点汇总,零基础必看哦

目录 V模型 W模型 H模型 总结&#xff1a; 软件测试有三种模型&#xff0c;分别是V模型&#xff0c;W模型和H模型。每种模型都有自己的优点和缺点。 V模型 V模型如下图所示&#xff1a; V模型的优点 V模型明确地标识出了在开发过程中一般应完成的测试级别&#xff0c;以及…

STM32-HAL库串口DMA空闲中断的正确使用方式+解析SBUS信号

STM32-HAL库串口DMA空闲中断的正确使用方式解析SBUS信号 一. 问题描述二. 方法一——使用HAL_UART_Receive_DMA三. 方法二——使用HAL_UARTEx_ReceiveToIdle_DMA四. 方法三——使用HAL_UARTEx_ReceiveToIdle_IT&#xff08;不使用DMA&#xff09;五. 总结 一. 问题描述 能够点…

java springboot整合MyBatis-Plus 多用点Plus支持一下国人开发的东西吧

文章java springboot整合MyBatis做数据库查询操作讲述了boot项目整合MyBatis的操作方法 但现在就还有一个 MyBatis-Plus Plus是国内整合的一个技术 国内的很多人会喜欢用 特别是一些中小型公司 他们用着会比较舒服 好 然后我们打开idea 创建一个项目 选择 Spring Initializr…

(九)CSharp-数组

一、矩形数组 1、访问数组元素 class Program{static void Main(string[] args){int[] intArr1 new int[15];intArr1[2] 10;int var1 intArr1[2];int[,] intArr2 new int[5, 10];intArr2[2, 3] 7;int var2 intArr2[2, 3];int[] myIntArray new int[4];for (int i 0; i…

Git 报错 Updates were rejected because the remote contains work that you do

目录 Git 报错 Updates were rejected because the remote contains work that you do 1、命令行出现这种情况 2、idea出现同样的报错&#xff0c;解决方式同上 Git 报错 Updates were rejected because the remote contains work that you do 这个报错实在是让我受不了了&…

Kendo UI for jQuery---03.组件___网格---05.编辑---01.概述

编辑概述 编辑是剑道 UI 网格的一项基本功能&#xff0c;它允许您操作其数据的呈现方式。 网格提供以下编辑模式&#xff1a; 批量编辑 内联编辑 弹出窗口编辑 自定义编辑开始 要启用编辑&#xff1a; 熟悉剑道UI中的常见编辑概念 配置网格的数据源 通过配置定义字段schem…

PaddleOCR Windows下配置环境并测试

目录 1.PaddleOCR 介绍 1.2 PaddleOCR支持模型介绍 2.环境配置 3.PaddleOCR源码 1.PaddleOCR 介绍 PaddleOCR旨在打造一套丰富、领先、且实用的OCR工具库&#xff0c;助力开发者训练出更好的模型&#xff0c;并应用落地。 支持多种OCR相关前沿算法&#xff0c;在此基础上打…

简单的一批的DockerFile构建(内附超详细docker学习笔记)

目录 介绍 DockerFile常用保留字指令 演示自定义构建java8版本centos docker专用学习笔记 超全 介绍 总结: 从应用软件的角度来看&#xff0c;Dockerfile、Docker镜像与Docker容器分别代表软件的三个不同阶段&#xff0c; * Dockerfile是软件的原材料 * Docker镜像是软件…

SpringBoot参数校验入门

一、添加依赖 <!--参数校验--> <dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId> </dependency> <!--lombok--> <dependency><groupId>org.projectlombok&…

few-shot object counting论文汇总

文章目录 2021OBJECT COUNTING: YOU ONLY NEED TO LOOK AT ONE 2022CounTR: Transformer-based Generalised Visual CountingFew-shot Object Counting with Similarity-Aware Feature Enhancement 2023CAN SAM COUNT ANYTHING? AN EMPIRICAL STUDY ON SAM COUNTING 2021 OBJ…

【MSP432电机驱动学习】TB6612带稳压电机驱动模块、MG310电机、13线霍尔编码器

所用控制板型号&#xff1a;MSP432P401r 今日终于得以继续我的电赛小车速通之路&#xff1a; 苏轼云 “ 素面常嫌粉涴 &#xff0c; 洗妆不褪朱红。 ” 这告诫我们不能只注重在表面粉饰虚伪的自己&#xff0c;要像梅花一样&#xff0c;不断磨砺自己的内在~ 后半句是 “…