Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 517|回复: 0

C#多线程编程(4)--异常处理+前三篇的总结

[复制链接]
  • TA的每日心情
    奋斗
    2024-11-24 15:47
  • 签到天数: 804 天

    [LV.10]以坛为家III

    2053

    主题

    2111

    帖子

    72万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    726782
    发表于 2021-6-10 13:05:28 | 显示全部楼层 |阅读模式

      本来是打算讲并行For和PLINQ的,但是我感觉前三篇我没有讲得很清晰。之前一直在看《CLR via C#》(后文简称CLR)的多线程部分,其中有些部分不是很明白,今天翻开《果壳中的C#》(后文简称果壳),看了下多线程部分,发现这本书讲的内容虽然很少,但是提纲挈领,把我之前读CLR中的知识点都串了起来。之前讲关键字async,await时,提到了状态机。其实,await会被编译成awaiter.GetAwaiter()方法,以及之后的委托,果壳中有很简单的例子来讲解,让我茅塞顿开,还有其他的部分也是这样。因此我决定写个总结,就是把之前我讲的我认为还没讲透的地方换种方式再讲一遍,目的是让大家,也是我自己真正的明白多线程的工作原理以及如何更好的使用异步。

      在总结之前,我要先介绍一下多线程的异常处理。多线程的异常处理分两种:非池化线程(自己new出来的线程)和池化线程(调用Task)。我们先来看一个非池化的例子。

    static void Main(string[] args)
    {
        try
        {
            Go();
        }
        catch (NullReferenceException exception)
        {
            //代码永远执行不到这里
        }
    }
    static void Go()
    {
        new Thread(() =>{
            Thread.Sleep(1000);
            throw new NullReferenceException();
        }).Start();
    }

    上述代码中,程序永远不会执行到catch里,因为当前try catch只能捕获到主线程中的异常,无法捕获其他线程中的异常。处理办法是将异常处理部分放到Go()函数中。

    在池化线程中,任务中抛出的异常都会被捕获,并收集到AggregateException中,例子如下

    static void Main(string[] args)
    {
        Go();
        Console.ReadLine();
    }
    static void Go()
    {
        try { Task.Run(() => throw new NullReferenceException()).Wait(); }
        catch (AggregateException ex){
            if (ex.InnerException is NullReferenceException)
                Console.WriteLine("捕获异常");
        }
    }

    若你用的是vs2013或者更低版本,当运行这段代码时,会弹出异常提示,再次点击运行,就能看到“捕获异常”,vs2015和vs2017则不需要,这是因为VS将遇到的异常弹出,是为了方便调试。这个例子可以看到任务中抛出了NullReferenceException异常,并在catch块中捕获了该异常。可以注意到异常的类型是AggregateException,当任务中抛出了多个异常,会存放在InnerExceptions中,这个和InnerException不同,这是一个只读集合,里面存放的是全部的异常。上述代码中,若不显示调用wait()方法,则不会捕获到异常。只有等待任务,或者尝试获取任务的返回值时,线程池才会抛出异常列表中的第一个异常。

    接下来是总结,说是总结,其实是将前面未讲透的知识点仔细讲解一下。

    先说一下async和await关键字。

    static void Main(string[] args){
        GoAsync();
        Console.WriteLine("异步运行");
        Console.ReadLine();
    }
    static async void GoAsync(){    
        await Task.Run(() => {
            //模拟其他任务
            Thread.Sleep(2000); });
        Console.WriteLine("任务结束");
    }

    可以看到程序运行了GoAsync()后,直接打印出了“异步运行”四个字,然后过了大约2秒才打印出任务结束。这表明GoAsync方法为异步方法,它不会阻塞线程,就是程序在执行到该函数后,不需要等待该方法结束,而是直接继续执行下一行代码。可以注意到GoAsync()方法标有async,且在Task.Run前添加了await关键字,这表明程序会等待Task任务,知道该任务结束后,才会继续执行。其实,这段代码会被编译器翻译成大概下面代码的样子(我省略了绝大部分代码,只保留关键的部分,若有兴趣,可以将该段代码编译后,调用IL反编译器,查看编译器真正的编译结果)。

    static void Main(string[] args){
        GoAsync();
        Console.WriteLine("异步运行");
        Console.ReadLine();
    }
    static void GoAsync(){            
        var awaiter = Task.Run(() => {
            //模拟其他任务
            Thread.Sleep(2000); }).GetAwaiter();
        awaiter.OnCompleted(() => Console.WriteLine("任务结束"));
    }

    调用await关键字,相当于在此处获取该任务的awaiter,该awaiter在任务结束后,会调用传入到OnCompleted方法中的委托。可以看到上述的写法没有async和await关键字优美,且没有办法标识GoAsync()方法为异步,只能以名字区分。async关键字能够很清晰的表明该方法是异步方法,且await用法简单,只要放在想要等待的任务前面就可以了,编译器会把await关键后面的部分放入到awaiter.OnCompleted()里面,等到任务结束后再开始执行。

    下面来介绍下TaskCompletionSource,该类型是用来实现线程的返回值问题的。TaskCompleteSource的结构大概是这样的:

    public class TaskCompletionSource<TResult>{
        public void SetResult(TResult result);
        public void SetException(Exception ex);
        public void SetCancel();
        public bool TrySetException(Exception ex);
        ...
    }

    每个Set方法都只能调用一次,再次调用会抛出异常,而Try方法会返回false。

    下面就利用TaskCompletionSource来实现我们自己的Run()方法:

    static void Main(string[] args)
    {
        GoAsync();
        Console.WriteLine("异步运行");
        Console.ReadLine();
    }
    static async void GoAsync()
    {
        var t = await Run(() =>
        {
            //模拟其他任务
            Thread.Sleep(2000);
            return "任务完成";
        });
        Console.WriteLine(t);
    }
    //自己的Run方法
    static Task<TResult> Run<TResult>(Func<TResult> func)
    {
        var tcs = new TaskCompletionSource<TResult>();
        ThreadPool.QueueUserWorkItem(t =>
        {
            try
            {
                tcs.SetResult(func());
            }
            catch (Exception ex)
            {
                tcs.SetException(ex);
            }
        }, null);
        return tcs.Task;
    }

    可以看到,Run()方法中,通过tcs.setResult()方法,成功的将返回值抛了出来,并返回了含有结果的Task。该段代码和上面的调用Task.Run的GoAsync()方法一样。也可以使用TaskCompletionSource加上定时器来实现Task.Delay()方法,而不用显式调用线程。该方法的实现就留给读者自行完成。

    以上,本文介绍了多线程中异常的捕获和处理,其中分为非池化线程和池化线程两种,其中非池化的异常处理要放在待执行的方法中,而池化线程可以通过调用Result或者await来将异常统一存放到AggregateException中统一处理。然后我针对前面三篇文章中没有讲透的点重新讲解了一下。包括await关键字的机制,编译器是通过Awaiter.OnComplete来实现的。之后是TaskCompletionSource,该类型是用来实现线程的返回值问题的,也讲解了如何实现自己的Task.Run()方法。并给读者留了一个用TaskCompletionSource和定时器来实现Task.Delay()的练手题。

    欢迎大家在我的评论区与我交流。

     

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-12-23 07:56 , Processed in 0.057033 second(s), 27 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表