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

        在 .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.mfbz.cn/a/28778.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【半监督图像分割 2023 CVPR】UniMatch

【半监督图像分割 2023 CVPR】UniMatch 论文题目&#xff1a;Revisiting Weak-to-Strong Consistency in Semi-Supervised Semantic Segmentation 中文题目&#xff1a;重新审视半监督语义分割中的强弱一致性 论文链接&#xff1a;https://arxiv.org/abs/2208.09910 论文代码&a…

功能测试常用的测试用例大全

登录、添加、删除、查询模块是我们经常遇到的&#xff0c;这些模块的测试点该如何考虑 1)登录 ① 用户名和密码都符合要求(格式上的要求) ② 用户名和密码都不符合要求(格式上的要求) ③ 用户名符合要求&#xff0c;密码不符合要求(格式上的要求) ④ 密码符合要求&#xff0c;…

大数据时代——生活、工作与思维的重大变革

最近读了维克托迈尔 – 舍恩伯格的《大数据时代》&#xff0c;觉得有不少收获&#xff0c;让我这个大数据的小白第一次理解了大数据。 作者是大数据的元老级先驱。 放一张帅照&#xff0c;膜拜下。 不过这本书我本人不推荐从头读一遍&#xff0c;因为书中的核心理念并不是特…

Mini热风枪 制作过程

首先引个流吧 立创开源广场&#xff1a;https://oshwhub.com/abby_qi/mini-re-feng-qiang 哔哩哔哩&#xff1a; 实物图 然后说一下硬件的选型和图 风扇&#xff1a;3010无刷风扇 额定电压3.7V&#xff08;其实这个风扇还有其他额定电压的&#xff0c;比如9V12V&#xff0c;…

linux文件的增量备份 Shell命令脚本

简单的增量备份脚本&#xff0c;自己用到了之后把部分择出来记录一下&#xff0c;方便日后查阅 # 昨天对应的月份 n_mon$(date -d -1day %Y%m) # 组合文件夹路径 path/home/admin/"$n_mon" # 昨天的0点作为增量备份起始时间&#xff0c;今日0点作为截止时间 s_date$…

【Java基础学习打卡07】Java语言概述

目录 前言一、Java语言1.Java语言简介2.Java语言优势3.Java能做什么&#xff1f; 二、Java之父三、Java简史1.Java版本时间线2.Java发展重要节点 总结 前言 本文主要了解Java语言&#xff0c;有哪些优势&#xff0c;能做什么。Java之父是谁&#xff1f;Java各版本的时间点及重…

mac版Excel表格中出现E+

相信很多人在使用Excel的时候都遇到过单元格变成###的情况&#xff0c;这是由于单元格列宽不够造成的&#xff0c;只需要增加列宽就可以正常显示。如果你在使用Excel的过程中遇到过出现"E"这种情况&#xff0c;此时不要惊慌&#xff0c;这是Excel自动对很大或很小的数…

Python进阶

文章目录 一、Python进阶&#xff1a;字符和编码1、字符编码的前世今生&#xff08;1&#xff09;、字符集概述&#xff08;2&#xff09;、几个基本概念&#xff08;3&#xff09;、字符编码的起源&#xff1a;ASCLL&#xff08;4&#xff09;、字符编码的发展&#xff1a;百家…

c4d云渲染几款好用的云渲染平台

C4D是指Maxon公司所开发的3D建模、动画和渲染软件Cinema 4D。它是一款非常流行的三维图形软件&#xff0c;被广泛用于电影、电视、游戏等领域中的动画制作、视觉效果、建筑可视化、工业设计、广告设计、虚拟现实等方面。其用户界面简单易用&#xff0c;功能丰富&#xff0c;可以…

《交通规划》——最短路分配方法

《交通规划》——最短路分配方法 说明&#xff1a;下面内容&#xff0c;将用python、networkx实现刘博航、杜胜品主编的《交通规划》P198页的例题&#xff0c;主要是实现最短路径分配方法。 1. 题目描述如下&#xff1a; 2. networkx构建网络 import networkx as nx import …

WRF进阶:使用ERA5-land数据驱动WRF/WRF撰写Vtable文件添加气象场

想用WRF模拟地气交换过程&#xff0c;对于WRF的地表数据&#xff0c;尤其是土壤温湿度数据要求便会很大&#xff0c;传统使用ERA5-singledata数据精度也许不足以满足需求&#xff0c;为此&#xff0c;本文尝试使用ERA5-land数据替换驱动WRF。 数据下载 ERA5-land的数据下载与…

springboot第27集:springboot-mvc,WxPay

在数据库中&#xff0c;DISTINCT 关键字用于查询去重后的结果集。它用于从查询结果中去除重复的行&#xff0c;只返回唯一的行。 要使用 DISTINCT 关键字&#xff0c;可以将其放置在 SELECT 关键字之前&#xff0c;指示数据库返回去重后的结果。 请注意&#xff0c;DISTINCT 关…

day07--java高级编程:JDK8的新特性,JDK9的新特性,JDK10的新特性,JDK11的新特性,JDK15的新特性

1 JDK8的其它新特性 说明&#xff1a;一些8中的新特性在&#xff0c;java高级部分学习的同时顺便讲过了。 1.1 JDK8新特性的总体结构 1.2 Java 8新特性简介 1.3 Lambda表达式 1.3.1 出现背景 1.3.2 Lambda表达式的使用举例 package com.atguigu.java1;import org.junit.Tes…

AntDB 企业增强特性介绍——AntDB在线数据扩容关键技术

数据库集群安装完成后&#xff0c;其数据存储容量是预先规划并确定的。随着时间的推移以及业务量的增加&#xff0c;数据库集群中的可用存储空间不断减少&#xff0c;面临数据存储容量扩充的需求。 传统的在线扩容的流程大致如下。 &#xff08;1&#xff09;在集群中加入新的 …

数据库迁移 | Oracle数据迁移方案之技术两三点

今年Oracle似乎又火了&#xff0c;火得要下掉&#xff0c;目前中国大概有240数据库企业&#xff0c;在国产信创的大趋势下&#xff0c;一片欣欣向荣&#xff0c;国库之春已然来临。到今天为止&#xff0c;Oracle依旧是市场份额最大的数据库&#xff0c;天下苦秦久矣&#xff0c…

【JVM 监控工具】JVisualVM的使用

文章目录 前言二、启动JVisualVM三、安装插件四、使用 前言 JVisualVM是一个Java虚拟机的监控工具&#xff0c;要是需要对JVM的性能进行监控可以使用这个工具哦 使用这个工具&#xff0c;你就可以监控到java虚拟机的gc过程了 那么&#xff0c;这么强大的工具怎么下载呢&…

顶奢好文:3W字,穿透Spring事务原理、源码,至少读10遍

说在前面 在40岁老架构师 尼恩的读者社区(50)中&#xff0c;最近有小伙伴拿到了一线互联网企业如阿里、美团、极兔、有赞、希音的面试资格&#xff0c;Spring事务源码的面试题&#xff0c;经常遇到&#xff1a; (1) spring什么情况下进行事务回滚&#xff1f; (2) spring 事务…

Transformer在CV领域有可能替代CNN吗?

目前已经有基于Transformer在三大图像问题上的应用&#xff1a;分类&#xff08;ViT&#xff09;&#xff0c;检测&#xff08;DETR&#xff09;和分割&#xff08;SETR&#xff09;&#xff0c;并且都取得了不错的效果。那么未来&#xff0c;Transformer有可能替换CNN吗&#…

索尼RSV视频修复方法论视频文件修复时样本文件的三同

索尼RSV类的文件修复案例有很多&#xff0c;程序操作也很简单没什么可说的&#xff0c;这次这个索尼ILCE-7SM3的案例就是为了让大家更好的认识视频修复中我称之为“三同“的重要性&#xff0c;想要恢复的效果好必须要把准备工作做到位。 故障文件:45.1G RSV文件 故障现象: 索…

工具篇--4 消息中间件-RabbitMq 模型介绍

1 介绍: RabbitMQ 是一个开源的消息中间件&#xff0c;它实现了 AMQP&#xff08;高级消息队列协议&#xff09;标准&#xff0c;并且支持多种语言和操作系统&#xff0c;包括 Java、Python、Ruby、PHP、.NET、MacOS、Windows、Linux 等等。RabbitMQ 提供了可靠的消息传递机制…