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

news2024/11/25 20:50:35

        本章继续介绍任务并行性,因篇幅所限,本章为中篇。


4、取消任务

        .NET Framework 提供了以下两个类来支持任务取消:

  • CancellationTokenSource :此类负责创建取消令牌,并将取消请求传递给通过源创建的所有令牌。

  • CancellationToken:侦听器使用该类来监视请求的当前状态。

        其实和 2.8 的内容有点类似,接下来按照教程步骤走一遍:

CancellationTokenSource source = new CancellationTokenSource();
CancellationToken token = source.Token;       

4.2、使用令牌创建任务

        创建任务的API就有很多了,可用 2.1 ~ 2.3 的任意一种:

var task = new Task(TestFunction.LoopFuntion, token);
var task = Task.Factory.StartNew(TestFunction.LoopFuntion, token);
var task = Task.Run(TestFunction.LoopFuntion, token);

        我设定的 LoopFuntion 是个无参函数,那么这个 token 有什么用呢?

        我一开始想的是,我运行了一个 Task,并传入了 token,他在后台长时间运行。当我需要取消任务时,则调用 CancellationTokenSource 的 Cancel 方法,我创建的 Task 就取消了 。但实际并不是这样。取消令牌只能在任务开始前取消任务,而任务一旦开始运行,则无法取消任务!

        其实和上一章说的 BackgroundWorker 类似,一旦开始执行就取消不了了,即便对 Task 进行 Dispose 也不行。需要程序员在函数中自行实现取消的方法,将方法修改如下:

        var task = Task.Run(() => TestFunction.LoopFuntion(token), token);
        
        public static async void LoopFuntion(CancellationToken token)
        {
            int index = 0;

            for (int i = 0; i < 10; i++)
            {
                await Task.Delay(1000);
                index++;
                Debug.Log($"LoopFuncion ,Number : {index}");

                if (token.IsCancellationRequested)
                {
                    Debug.Log("手动取消!");
                    return;//手动取消;
                }
            }
        }

4.3、注册请求取消的回调

        CancellationToken 中可以注册一个回调函数,在取消时触发。同样在上述的LoopFunction中,可以如下写代码:

//注册一个事件,在 token 设置为取消时触发中断循环
bool IsCancelled = false;
token.Register(() =>
{
    IsCancelled = true;
});

5、等待正在运行的任务

        TPL 中提供了多种用于等待一个或多个任务的 API,具体如下所示:

5.1、Task.Wait

        我们先写一个简单示例如下:

        private void RunWithTaskWait()
        {
            Debug.Log("RunWithTaskWait Start !");
            var task = Task.Run(TestFunction.DebugWithTaskDelay);
            Debug.Log("子线程已经开始运行 !");
            task.Wait();
            Debug.Log("RunWithTaskWait End!");
        }

        我在Unity主线程中调用,猜猜结果会如何?可能你会认为最后一条Log(“RunWithTaskWait End!”)会在 task 完全执行完成之后才会打印,然而其实并不会:

         这个效果和没有 Wait 的效果是一样的。

        这个和书上说的就不一样了,无语……我在想作者写书的时候是不是根本没有运行过啊……总之我们先不纠结这个,先看看怎么实现 Task 的等待:

        RunWithTaskWait 不变,然后 DebugWithTaskDelay 做如下修改:

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

        发现区别没有?我们把返回值从 void 直接改成 Task,没有编译错误,而且运行结果也正确了。(我这样写,在等待的时候会阻塞 Unity 主线程,大家可以在另一个 Task 调用 DebugWithTaskDelay)

         其实从C#的源代码上可以看到,Task.Run 有以下两个重载:

public static Task Run(Func<Task> function);//函数在 Task 调度中执行
public static Task Run(Action action);//函数当成普通任务执行

        调用下面这个 Run 是不会有效果的,类似于普通函数,只有上面那个 Run 才是正确的。所以我们需要返回一个 Task 才能正确执行任务里的逻辑。而 Task 又比较特殊,和 void 一样不用 return !这个确实是我没有想到的,真有你的 C# !不得不说,我对 Task 的理解又更进一步 ~

5.2、Task.WaitAll

        Task.WaitAll 用于等待多个任务,任务将作为数组的传递给方法,并且调用程序将被阻塞,直至所有任务都完成。Task.WaitAll 还支持超时和取消令牌。

        在 TestFunction 中添加一个传参的等待方法:

        /// <summary>
        /// 传参的等待方法;
        /// </summary>
        /// <param name="millisecondsDelay">毫秒</param>
        /// <returns></returns>
        public static async Task DebugWithTaskWaitByParameter(int millisecondsDelay)
        {
            Debug.Log($"开始等待:{millisecondsDelay} !");
            await Task.Delay(millisecondsDelay);
            Debug.Log($"结束等待:{millisecondsDelay} !");
        }

        之后我们尝试执行 Task.WallAll,这里我为了避免卡死主线程,所以开了一个新线程来执行:

        private void RunWithTaskWaitAll()
        {
            Thread thread = new Thread(() =>
                {
                    Debug.Log($"开始 RunWithTaskWaitAll!");
                    var task1 = Task.Run(() => TestFunction.DebugWithTaskWaitByParameter(1000));
                    var task2 = Task.Run(() => TestFunction.DebugWithTaskWaitByParameter(2000));
                    var task3 = Task.Run(() => TestFunction.DebugWithTaskWaitByParameter(3000));
                    Task.WaitAll(task1, task2, task3);
                    Debug.Log($"完成 RunWithTaskWaitAll!");
                });
            thread.Start();
        }

        效果如下:

        和预期效果一样。

5.3、Task.WaitAny

        Task.WaitAny 也可以等待多个任务,顾名思义,只要等待的任务中有任何一个执行完毕,调用线程就不会阻塞。 Task.Wait、Task.WaitAll 和 Task.WaitAny 都可以设置超时时间和取消令牌,这个就是纯 API 的调用,没有什么变化,就不再赘述了。

        Task.WaitAny 的测试代码如下:

 

        private void RunWithTaskWaitAny()
        {
            Thread thread = new Thread(() =>
            {
                Debug.Log($"开始 RunWithTaskWaitAny!");
                var task1 = Task.Run(() => TestFunction.DebugWithTaskWaitByParameter(1000));
                var task2 = Task.Run(() => TestFunction.DebugWithTaskWaitByParameter(2000));
                var task3 = Task.Run(() => TestFunction.DebugWithTaskWaitByParameter(3000));
                Task.WaitAny(task1, task2, task3);
                Debug.Log($"完成 RunWithTaskWaitAny!");
            });
            thread.Start();
        }

        结果如下:

 

5.4、Task.WhenAll

        Task.WhenAll 是 Task.WatiAll 方法的非阻塞变体,区别在于会返回一个 Task ,代表所有指定任务的等待。单看概念可能不好理解,直接上代码:

        private void Update()
        {
            if (RunningTask == null)
                return;

            switch (RunningTask.Status)
            {
                case TaskStatus.RanToCompletion:
                    Debug.Log("RunningTask 运行完成!");
                    RunningTask.Dispose();
                    RunningTask = null;
                    break;
            }
        }

        private Task RunningTask;

        private void RunWithTaskWhenAll()
        {
            Thread thread = new Thread(() =>
            {
                Debug.Log($"开始 RunWithTaskWhenAll!");
                var task1 = Task.Run(() => TestFunction.DebugWithTaskWaitByParameter(1000));
                var task2 = Task.Run(() => TestFunction.DebugWithTaskWaitByParameter(2000));
                var task3 = Task.Run(() => TestFunction.DebugWithTaskWaitByParameter(3000));
                RunningTask = Task.WhenAll(task1, task2, task3);
                Debug.Log($"完成 RunWithTaskWhenAll!");
            });
            thread.Start();
        }

        这里开了一个 Update 来轮询 Task.WhenAll 返回的 Task 的结果,当 RunningTask 完成时进行一次打印。其余部分都是把 5.2 的 Task.WaitAll 测试用例抄过来的。

        结果如下:

         结果很明显,开始执行3个等待任务的时候,并没有阻塞调用线程。而等到3个任务都完成之后,RunningTask 才会标记为完成。

5.5、Task.WhenAny

        这个很显然了,就是 WaitAny 的非阻塞变体。WaitAny 和 WhenAny 的区别就和 WaitAll 和 WhenAll 的区别一样,就是功能的排列组合,这里就不再赘述了。

6、处理任务异常

        所有优秀的程序员都擅长高效地处理异常,这也是并行编程最重要的方面之一。任务并行库(TPL)提供了一种高效的设计来处理异常:任务中发生的任何未处理异常都将被延迟,然后传播到使用 Join 方法加入的线程,后者负责观察任务中的异常。

        下面我们通过代码实例来学习:

6.1、处理来自单个任务的异常

        首先,我们需要写一个会出异常的程序:

        /// <summary>
        /// 一个“可能”错误的程序;
        /// 会抛出异常错误
        /// </summary>
        public async static Task ErrorFunction()
        {
            var random = new System.Random();
            int div = random.Next(-2, 2);
            float ret = 1;
            for (int i = 0; i < 10; i++)
            {
                if (div == 0)
                {
                    //这里我们只打印,但是并不中断运行;
                    Debug.LogError("开始除0了!");
                }

                //直接除法,抛出除0的移除
                ret += i / div;
                await Task.Yield();
                div = random.Next(-2, 2);
            }
            Debug.Log($"ErrorFunction 居然成功完成了!结果为:{ret} | {div}");
        }

        之后我们直接运行这段程序,就按照最简单的 Task.Run 来运行。结果很有意思啊:

         发现没有,已经出现除0的警告了,但是并没有跑错误出来,Unity 一点反应没有!这说明在子线程里的异常是不会直接抛给主线程的。

        下面我们换一个写法:

        private void RunWithErrorTask()
        {
            try
            {
                Debug.Log("RunWithErrorTask 开始!");
                var task=Task.Run(TestFunction.ErrorFunction);
                task.Wait();//不用 task.Wait() 则不会抛出异常
            }
            catch (System.Exception ex)
            {
                Debug.LogError(ex.Message);
                Debug.LogError(ex.StackTrace);
                Debug.LogError(ex.InnerException);
            }
        }

        我们调用 task.Wait(),用 try catch 语句进行包裹,结果如下:

         其实没啥好说的,就是因为 task.Wait 调回了主线程,所以能接收到异常。上面2张截图,其实就是为了说明 Exception 的 StackTrace 和 InnerException 的区别:可以看到 StackTrace 是没有行号的,但是 InnerException 是可以定位到具体的方法。

6.2、处理来自多个任务的异常

        类似于 5.3 那种,子任务有多个的情况,异常处理也类似。把 catch 的类型换成 AggregateException 就能拿到所有的异常了。

        这里就不贴代码了,只要一张贴图就能明白所有:

 

6.3、使用回调函数处理任务异常

        这里指的就是 AggregateException 运行使用回调来处理异常:

            .......
            catch (System.AggregateException ex)
            {
                ex.Handle(exception =>
                {
                    Debug.LogError(exception.InnerException);
                    return true;
                });
            }

        这里就是 Handle 提供一个方法,返回 true 表示此异常已经正确处理,返回 false 则系统会再次抛出此异常。

        这些都是通用的 C# 函数异常处理方法了,就不必要再多说了。

AggregateException.Handle(FuncInvokes a handler on each Exception contained by this AggregateException. icon-default.png?t=N5F7https://learn.microsoft.com/en-us/dotnet/api/system.aggregateexception.handle?view=netstandard-2.1


            本文介绍了取消任务、等待任务、任务异常收集的基本写法、语法介绍等。这些都是平时进行 TPL 编程时常用的 API ,我只是简单介绍一下。后续熟练使用还是要多多练习。

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

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

相关文章

关于xinput1_3.dll丢失的详细解决方法

xinput1_3.dll是电脑文件中的dll文件&#xff08;动态链接库文件&#xff09;。如果计算机中丢失了某个dll文件&#xff0c;可能会导致某些软件和游戏等程序无法正常启动运行&#xff0c;并且导致电脑系统弹窗报错。 在我们打开软件或者游戏的时候&#xff0c;电脑提示xinput1_…

8、共享模型之工具

目录 8.1 线程池2、ThreadPoolExecutor&#xff08;及其重要&#xff09;1) 线程池状态2) 构造方法3) newFixedThreadPool4) newCachedThreadPool5) newSingleThreadExecutor6) 提交任务7) 关闭线程池8) 任务调度线程池 8.1 线程池 2、ThreadPoolExecutor&#xff08;及其重要…

中国电子学会2023年05月份青少年软件编程Python等级考试试卷六级真题(含答案)

2023-05 Python六级真题 分数&#xff1a;100 题数&#xff1a;38 测试时长&#xff1a;60min 一、单选题(共25题&#xff0c;共50分) 1. 明明每天坚持背英语单词&#xff0c;他建立了英语单词错题本文件“mistakes.txt”&#xff0c;将每天记错的单词增加到该文件中&…

DuiLib的编译和运行

文章目录 1、原生DuiLib的编译和运行1.1、下载并解压成这个样子1.2、打开.sln解决方案文件1.3、编译成功 2、网易DuiLib编译和运行2.1、下载并解压成这个样子2.2、打开.sln解决方案文件2.3、编译成功 3、腾讯DuiLib编译和运行vs 20173.1、下载并解压成这个样子3.2、打开.sln解决…

使用 Node.js、K8s 和分布式 SQL 构建世界上最具弹性的待办事项列表应用程序

本文演示了如何使用 Kubernetes (K8s) 和分布式 SQL 构建云原生 Node.js 应用程序。 开发可扩展且可靠的应用程序是一项热爱的工作。一个云原生系统可能包括单元测试、集成测试、构建测试&#xff0c;以及用于构建和部署应用程序的完整管道&#xff0c;只需单击一个按钮即可。 …

【计算机网络】第二章应用层-电子科技大学2023期末考试

第二章 应用层 应用层协议原理 网络应用程序体系结构 客户机/服务器体系结构&#xff1a;至少有一个服务器&#xff0c;一个客户机&#xff0c;其中服务器总是打开的&#xff0c;具有固定的众所周知的IP地址&#xff0c;主机群集常被用于创建强大的虚拟服务器&#xff0c;而客…

【Trino实战】Trino下ORC与Parquet查询性能分析

Trino下ORC与Parquet查询性能分析 环境 OS&#xff1a;CentOS 6.5 JDK&#xff1a;1.8 内存&#xff1a;256G 磁盘&#xff1a;HDD CPU&#xff1a;Dual 8-core Intel Xeon CPU (32 Hyper-Threads) E5-2630 v3 2.40GHz HDFS&#xff1a;2.9.2 Hive&#xff1a;2.3.9 T…

[n00bzCTF 2023] CPR 全

Crypto AES 给了java的加密原码&#xff0c;AES加密&#xff0c;有key import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; import java.n…

arima模型原理及实战

目录 1&#xff0c;概念 2&#xff0c;数学知识 3&#xff0c;前提条件 4&#xff0c;序列不平稳时的平稳性方法 5&#xff0c;模型定阶&#xff0c;确定P和Q 6&#xff0c;模型训练与检验 1&#xff0c;概念 ARIMA模型&#xff08;英语&#xff1a;Autoregressive Integr…

grep及文本处理命令

正则表达式 一.基础命令 1.grep命令 对文本的内容进行过滤&#xff0c;针对行处理 1.1grep格式 grep [选项]…查找条件 目标文件 1.2grep命令选项 -m数字——————匹配几次后停止eg&#xff1a;grep -m 1 root /etc/passwd————————————多个匹配只取 -v …

【编程语言 · C语言 · 递归函数】

递归函数 C 语言的函数都支持递归, 也就是说&#xff0c;每个函数都可以直接或者间接第调用自己。所谓的间接调用&#xff0c;是指在递归函数调用的下层函数中再调用自己。 递归关系图如下&#xff1a; 递归之所以能实现&#xff0c;是因为函数的每个执行过程在栈中都有自己的…

深入理解 SpringBoot 日志框架:从入门到高级应用——(三)Logback 输出日志到 MySQL 数据库

文章目录 添加依赖导入 SQL 文件配置 logback-spring.xml运行结果 Logback 是一个开源的日志框架&#xff0c;它支持多种日志输出方式&#xff0c;包括控制台输出、文件输出、邮件输出等。如果要将 Logback 输出的日志保存到 MySQL 数据库中&#xff0c;可以按照以下步骤进行配…

Tapd在研发团队中的使用技巧-持续更新ing

1.TAPD第三方服务集成能力&#xff0c;支持与代码仓库、流水线进行了深度打通&#xff0c;力求为开发团队提供流畅高效的使用体验。我们梳理了一份攻略&#xff0c;掌握下面几个小技能&#xff0c;让TAPD与代码仓库、流水线一起&#xff0c;成为研发团队的得力助手&#xff0c;…

数据结构算法刷题(27)回溯(子集型)

回溯思想&#xff1a; 思路&#xff1a;这种出现全部xx组合的&#xff0c;基本都是回溯算法。首先&#xff0c;当digits是空&#xff0c;那返回也是空。当回溯到边界条件的时候&#xff0c;就更新答案&#xff0c;在非边界条件的时候&#xff0c;循环该数值下的全部情况。 cla…

125760-33-0,Fmoc-Thr(Ac4Galβ1-3Ac2GalNAcα)-OH,于蛋白质糖基化修饰

文章关键词&#xff1a;糖化学试剂&#xff0c;化学试剂&#xff0c;糖基氨基酸一、试剂基团反应特点&#xff08;Reagent group reaction characteristics&#xff09;&#xff1a; Fmoc-Thr(Ac4Galβ1-3Ac2GalNAcα)-OH中蛋白质糖基化修饰是在糖基转移酶的催化作用下糖链分子…

色环电阻介绍

复习一下色环电阻&#xff0c;是在电阻封装上(即电阻表面)涂上一定颜色的色环&#xff0c;来代表这个电阻的阻值。色环实际上是早期为了帮助人们分辨不同阻值而设定的标准。色环电阻现在应用还是很广泛的&#xff0c;如家用电器、电子仪表、电子设备中常常可以见到。但由于色环…

Java内存模型(JMM)和volatile原理

一、Java 内存模型 JMM即Java Memory Model&#xff0c;他定义了主存&#xff08;共享的数据&#xff09;、工作内存&#xff08;私有的数据&#xff09;抽象概念&#xff0c;底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等 JMM体现以下几个方面 原子性-保证指令不会受…

ad18报错:Minimum Solder Mask Sliver Constraint

报告上提示&#xff1a; Minimum Solder Mask Sliver (Gap0.254mm) (All),(All) Minimum Solder Mask Sliver Constraint&#xff0c;PCB焊盘阻焊层之间间距小于0.254报错 修改了这里&#xff0c;把这个报警值改小一些&#xff0c;就不会报警了 翻译过来是&#xff1a;最小…

8.vue3医疗在线问诊项目 - _问诊室模块-websocket学习 ==> 消息卡片、websocket、socket.io、约定通讯规则、建立连接

8.vue3医疗在线问诊项目 - _问诊室模块-websocket学习 &#xff1e; 消息卡片、websocket、socket.io、约定通讯规则、建立连接 问诊室-路由与组件 目标&#xff1a;配置路由和分析结构 1&#xff09;路由配置 {path: /room,component: () > import(/views/room/index.vue)…

UNIX网络编程卷一 学习笔记 第二十章 广播

本书迄今为止的所有例子都是单播&#xff1a;一个进程与另一个进程通信。TCP只支持单播寻址&#xff0c;而UDP和原始IP还支持其他寻址类型&#xff0c;下图比较了不同的寻址方式&#xff1a; IPv6往寻址体系中增加了任播&#xff08;anycasting&#xff09;方式。RFC 1546讲述…