详解C#异步多线程使用中的常见问题

目录
  • 异常处理
  • 线程取消
  • 临时变量
  • 线程安全

异常处理

小伙伴有没有想过,多线程的异常怎么处理,同步方法内的异常处理,想必都非常非常熟悉了。那多线程是什么样的呢,接着我讲解多线程的异常处理

首先,我们定义个任务列表,当 11、12 次的时候,抛出一个异常,最外围使用 try catch 包一下

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    try
    {
        TaskFactory taskFactory = new TaskFactory();
        List<Task> tasks = new List<Task>();
        for (int i = 0; i < 20; i++)
        {
            string name = $"第 {i} 次";

            Action<object> action = t =>
            {
                Thread.Sleep(2 * 1000);
                if (name.ToString().Equals("第 11 次"))
                {
                    throw new Exception($"{t},执行失败");
                }
                if (name.ToString().Equals("第 12 次"))
                {
                    throw new Exception($"{t},执行失败");
                }
                Console.WriteLine($"{t},执行成功");
            };

            tasks.Add(taskFactory.StartNew(action, name));
        }
    }
    catch (AggregateException aex)
    {
        foreach (var item in aex.InnerExceptions)
        {
            Console.WriteLine("Main AggregateException:" + item.Message);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("Main Exception:" + ex.Message);
    }

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

启动程序,可以看到 vs 捕获到了异常的代码行,但 catch 并未捕获到异常,这是为什么呢?是因为线程里面的异常被吞掉了,从运行的结果也可以看到,main end 在子线程没有执行任时就已经结束了,那说明 catch 已经执行过去了。

那有没有办法捕获多线程的异常呢?答案:有的,等待线程完成计算即可

看下面代码,有个特殊的地方 AggregateException.InnerExceptions 专门为多线程准备的,可以查看多线程异常信息

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    try
    {
        TaskFactory taskFactory = new TaskFactory();
        List<Task> tasks = new List<Task>();
        for (int i = 0; i < 20; i++)
        {
            string name = $"第 {i} 次";

            Action<object> action = t =>
            {
                Thread.Sleep(2 * 1000);
                if (name.ToString().Equals("第 11 次"))
                {
                    throw new Exception($"{t},执行失败");
                }
                if (name.ToString().Equals("第 12 次"))
                {
                    throw new Exception($"{t},执行失败");
                }
                Console.WriteLine($"{t},执行成功");
            };

            tasks.Add(taskFactory.StartNew(action, name));
        }

        Task.WaitAll(tasks.ToArray());
    }
    catch (AggregateException aex)
    {
        foreach (var item in aex.InnerExceptions)
        {
            Console.WriteLine("Main AggregateException:" + item.Message);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("Main Exception:" + ex.Message);
    }

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

启动线程,可以看到任务全部执行完毕,且 AggregateException.InnerExceptions 存储了,子线程执行时的异常信息

但 WaitAll 不好,总不能一直 WaitAll 吧,它会卡界面。并不适用于异步场景对吧,接着来看另外一直解决方案。就是子线程里不允许出现异常,如果有自己处理好,即 try catch 包一下,平时工作中建议这么做。

使用 try catch 将子线程执行的代码包一下,且在 catch 打印错误信息

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    try
    {
        TaskFactory taskFactory = new TaskFactory();
        List<Task> tasks = new List<Task>();
        for (int i = 0; i < 20; i++)
        {
            string name = $"第 {i} 次";

            Action<object> action = t =>
            {
                try
                {
                    Thread.Sleep(2 * 1000);
                    if (name.ToString().Equals("第 11 次"))
                    {
                        throw new Exception($"{t},执行失败");
                    }
                    if (name.ToString().Equals("第 12 次"))
                    {
                        throw new Exception($"{t},执行失败");
                    }
                    Console.WriteLine($"{t},执行成功");
                }
                catch (Exception ex)
                {
					Console.WriteLine(ex.Message);
                }
            };

            tasks.Add(taskFactory.StartNew(action, name));
        }
    }
    catch (AggregateException aex)
    {
        foreach (var item in aex.InnerExceptions)
        {
            Console.WriteLine("Main AggregateException:" + item.Message);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("Main Exception:" + ex.Message);
    }

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

启动程序,可以看到任务全部执行,且子线程异常也捕获到

线程取消

有时候会有这样的场景,多个任务并发执行,如果某个任务失败了,通知其他的任务都停下来。首先打个预防针 Task 在外部无法中止的,Thread.Abort 不靠谱。其实线程取消的这个想法是错误的,线程是 OS 的资源,程序是无法掌控什么时候取消,发出一个动作可能立马取消,也可能等 1 s 取消。

解决方案:线程自己停止自己,定义公共的变量,修改变量状态,其他线程不断检测公共变量

例如:CancellationTokenSource 就是公共变量,初始化为 false 状态,程序执行 CancellationTokenSource .Cancel() 方法会取消,其他线程检测到 CancellationTokenSource .IsCancellationRequested 会是取消状态。CancellationTokenSource.Token 在启动 Task 时传入,如果已经 CancellationTokenSource.Cancel() ,这个任务会放弃启动,抛出一个异常的形式放弃。

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    try
    {
        TaskFactory taskFactory = new TaskFactory();
        List<Task> tasks = new List<Task>();
        CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); // bool 

        for (int i = 0; i < 20; i++)
        {
            string name = $"第 {i} 次";

            Action<object> action = t =>
            {
                try
                {
                    Thread.Sleep(2 * 1000);
                    if (name.ToString().Equals("第 11 次"))
                    {
                        throw new Exception($"{t},执行失败");
                    }
                    if (name.ToString().Equals("第 12 次"))
                    {
                        throw new Exception($"{t},执行失败");
                    }
                    if (cancellationTokenSource.IsCancellationRequested) // 检测信号量
                    {
                        Console.WriteLine($"{t},放弃执行");
                        return;
                    }
                    Console.WriteLine($"{t},执行成功");
                }
                catch (Exception ex)
                {
                    cancellationTokenSource.Cancel();
                    Console.WriteLine(ex.Message);
                }
            };

            tasks.Add(taskFactory.StartNew(action, name,cancellationTokenSource.Token));
        }

        Task.WaitAll(tasks.ToArray());
    }
    catch (AggregateException aex)
    {
        foreach (var item in aex.InnerExceptions)
        {
            Console.WriteLine("Main AggregateException:" + item.Message);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("Main Exception:" + ex.Message);
    }

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

启动程序,可以看到 11、12 此任务失败,18、19 放弃了任务执。有的小伙伴疑问了,12 之后的部分为什么执行成功了,因为 CPU 是分时分片的吗,会有延迟,延迟少不了。

临时变量

首先看个代码,循环 5 次,多线程的方式,依次输出序号

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    for (int i = 0; i < 5; i++)
    {
        Task.Run(() => {
            Console.WriteLine(i);
        });
    }

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

启动程序,不是我们预期的结果 0、1、2、3、4,为什么是 5 个 5 呢?因为全程只有一个 i ,当主线程执行完毕时 i = 5 ,但子线程可能还没有开始执行任务,轮到子线程取 i 时,已经是主线程 1 循环完毕后的 5 了。

改造代码:在 for 循环内加一行代码 int k = i,且在子线程用的变量也改为 k

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    for (int i = 0; i < 5; i++)
    {
        int k = i;
        Task.Run(() => {
            Console.WriteLine($"k={k},i={i}");
        });
    }

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

启动程序,可以看到是我们预期的结果 0、1、2、3、4,为什么会这样子呢?因为全程有 5 个 k,每次循环都会创建一个 k 存储当前的 i,不同的子线程使用的也是,每次循环的 i 值。

线程安全

首先为什么会有线程安全的概念呢?首先我们来看一个正常程序,如下

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    int TotalCount = 0;
    List<int> vs = new List<int>();

    for (int i = 0; i < 10000; i++)
    {
        TotalCount += 1;
        vs.Add(i);
    }

    Console.WriteLine(TotalCount);
    Console.WriteLine(vs.Count);

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

启动程序,可以看到循环 10000 次,最终的求和与列表里的数据量都是 10000,这是正常的

接着,将求和与添加列表,换成多线程,等待全部线程完成工作后,打印信息

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    int TotalCount = 0;
    List<int> vs = new List<int>();

    TaskFactory taskFactory = new TaskFactory();
    List<Task> tasks = new List<Task>();

    for (int i = 0; i < 10000; i++)
    {
        int k = i;
        tasks.Add(taskFactory.StartNew(() =>
        {
            TotalCount += 1;
            vs.Add(i);
        }));
    }

    Task.WaitAll(tasks.ToArray());

    Console.WriteLine(TotalCount);
    Console.WriteLine(vs.Count);

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

启动程序,可以看到,两个结果都不是 10000 呢?这就是线程安全

因为 TotalCount 是个共享的变量,当多个线程去取 TotalCount 进行 +1 后,线程都去放值的时候,后一个线程会替换掉前一个线程放置的值,所以就会形成做最终不是 10000 的结果。列表,可以看做是一个连续的块,当多线程添加的时候,也会进行覆盖。

如何解决呢?答案:lock、安全队列、拆分合并计算。下面对 lock 进行讲解,安全队列与拆分合并计算,有兴趣的小伙伴可以私下交流

1 .lock
第一种,通过加锁的方式,这种也是日常工作总常用的一种。首先定义个私有的静态引用类型的变量,然后将需要锁的运算放到 lock () 方法内

在 { } 内同一时刻,只有一个线程执行,所以尽可能 {} 放置必要的逻辑运行提高效率。lock 只能锁引用类型,原理是占用这个引用链接。不要用 string 会享元,即如 lock() 是相同的字符串,无论定义多少个变量,其实都是一个。

internal class Program
{
    private static readonly object _lock = new object();

    static void Main(string[] args)
    {
        Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

        int TotalCount = 0;
        List<int> vs = new List<int>();

        TaskFactory taskFactory = new TaskFactory();
        List<Task> tasks = new List<Task>();

        for (int i = 0; i < 10000; i++)
        {
            int k = i;
            tasks.Add(taskFactory.StartNew(() =>
            {
                lock (_lock)
                {
                    TotalCount += 1;
                    vs.Add(i);
                }
            }));
        }

        Task.WaitAll(tasks.ToArray());

        Console.WriteLine(TotalCount);
        Console.WriteLine(vs.Count);

        Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

        Console.ReadLine();
    }
}

启动程序,可以看到,此时在多线程的情况下,最终的结果是正常的

这段代码,是官方推荐写法 private 防止外面也被引用,static 保证全场唯一

private static readonly object _lock = new object();

扩展:与 lock 等价的有个 Monitor,用法如下

private static object _lock = new object();

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    int TotalCount = 0;
    List<int> vs = new List<int>();

    TaskFactory taskFactory = new TaskFactory();
    List<Task> tasks = new List<Task>();

    for (int i = 0; i < 10000; i++)
    {
        int k = i;
        tasks.Add(taskFactory.StartNew(() =>
        {
            Monitor.Enter(_lock);
            TotalCount += 1;
            vs.Add(i);
            Monitor.Exit(_lock);
        }));
    }

    Task.WaitAll(tasks.ToArray());

    Console.WriteLine(TotalCount);
    Console.WriteLine(vs.Count);

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

到此这篇关于详解C#异步多线程使用中的常见问题的文章就介绍到这了,更多相关C#异步多线程内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入分析C#中的异步和多线程

    许多开发人员对异步代码和多线程以及它们的工作原理和使用方法都有错误的认识.在这里,你将了解这两个概念之间的区别,并使用c#实现它们. 我:"服务员,这是我第一次来这家餐厅.通常需要4个小时才能拿到食物吗?" 服务员:"哦,是的,先生.这家餐厅的厨房里只有一个厨师." 我:"--只有一个厨师吗?" 服务员:"是的,先生,我们有好几个厨师,但每次只有一个在厨房工作." 我:"所以其他10个穿着厨师服站在厨房里的人--什么

  • C# 异步多线程入门到精通之Thread篇

    上一篇:C# 异步多线程入门基础 下一篇:C# 异步多线程入门到精通之ThreadPool篇 Thread API 这里对 Thread 的一些常用 API 进行介绍,使用一些案例进行说明.由于 Thread 的不可控与效率问题,Thread 现在已经不常用了,这里介绍一些 API ,想更深入的同学可以继续研究研究. Instance 首先看 Thread 的构造函数,有 ThreadStart .ParameterizedThreadStart .maxStackSize 类型的参数,这三个常

  • C# 异步多线程入门基础

    目录 进程.线程 1. 进程 2. 线程 分时.分片 同步.异步 异步.多线程 异步多线程效率 多线程无序性 扩展 异步多线程版本 下一篇:C# 异步多线程入门到精通之Thread篇 进程.线程 1. 进程 首先了解,什么是线程? 即一个应用程序运行时,占用资源的综合是一个进程.Windows 任务管理器里面可以看到,里面一个个都是在运行的进程. 2. 线程 线程是执行流的最小单位.线程其实是看不到的,其实也可以,例如 Windows 任务管理器:正在运行 272 个进程,272 个进程运行了

  • C#多线程与异步的区别详解

    C#多线程与异步的区别详解 随着拥有多个硬线程 CPU(超线程.双核)的普及,多线程和异步操作等并发程序设计方法也受到了更多的关注和讨论.本文主要是想与各位高手一同探讨一下如何使用并发来最大化程序的性能. 多线程和异步操作的异同 多线程和异步操作两者都可以达到避免调用线程阻塞的目的,从而提高软件的可响应性.甚至有些时候我们就认为多线程和异步操作是等同的概念.但是,多线程和异步操作还是有一些区别的.而这些区别造成了使用多线程和异步操作的时机的区别. 异步操作的本质 所有的程序最终都会由计算机硬件来

  • 解析C#多线程编程中异步多线程的实现及线程池的使用

    0.线程的本质 线程不是一个计算机硬件的功能,而是操作系统提供的一种逻辑功能,线程本质上是进程中一段并发运行的代码,所以线程需要操作系统投入CPU资源来运行和调度. 1.多线程: 使用多个处理句柄同时对多个任务进行控制处理的一种技术.据博主的理解,多线程就是该应用的主线程任命其他多个线程去协助它完成需要的功能,并且主线程和协助线程是完全独立进行的.不知道这样说好不好理解,后面慢慢在使用中会有更加详细的讲解. 2.多线程的使用: (1)最简单.最原始的使用方法:Thread oGetArgThre

  • C# 异步多线程入门到精通之ThreadPool篇

    上一篇:C# 异步多线程入门到精通之Thread篇 下一篇:异步多线程之入Task,待更新 启动线程池线程 ThreadPool 提供的 API 相对于 Thread 是比较少的,在 ThreadPool 中需使用 QueueUserWorkItem 方法,来启动一个线程 例如:Dosome 是个普通的方法,传入 QueueUserWorkItem 方法开启新线程执行此方法 public static void Dosome() { Console.WriteLine($"Task Start

  • 详解C#异步多线程使用中的常见问题

    目录 异常处理 线程取消 临时变量 线程安全 异常处理 小伙伴有没有想过,多线程的异常怎么处理,同步方法内的异常处理,想必都非常非常熟悉了.那多线程是什么样的呢,接着我讲解多线程的异常处理 首先,我们定义个任务列表,当 11.12 次的时候,抛出一个异常,最外围使用 try catch 包一下 static void Main(string[] args) { Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.Manage

  • 详解Java中多线程异常捕获Runnable的实现

    详解Java中多线程异常捕获Runnable的实现 1.背景: Java 多线程异常不向主线程抛,自己处理,外部捕获不了异常.所以要实现主线程对子线程异常的捕获. 2.工具: 实现Runnable接口的LayerInitTask类,ThreadException类,线程安全的Vector 3.思路: 向LayerInitTask中传入Vector,记录异常情况,外部遍历,判断,抛出异常. 4.代码: package step5.exception; import java.util.Vector

  • 详解JS异步加载的三种方式

    一:同步加载 我们平时使用的最多的一种方式. <script src="http://yourdomain.com/script.js"></script> <script src="http://yourdomain.com/script.js"></script> 同步模式,又称阻塞模式,会阻止浏览器的后续处理,停止后续的解析,只有当当前加载完成,才能进行下一步操作.所以默认同步执行才是安全的.但这样如果js中有输

  • 详解JavaScript 异步编程

    异步的概念 异步(Asynchronous, async)是与同步(Synchronous, sync)相对的概念. 在我们学习的传统单线程编程中,程序的运行是同步的(同步不意味着所有步骤同时运行,而是指步骤在一个控制流序列中按顺序执行).而异步的概念则是不保证同步的概念,也就是说,一个异步过程的执行将不再与原有的序列有顺序关系. 简单来理解就是:同步按你的代码顺序执行,异步不按照代码顺序执行,异步的执行效果更高: 以上是关于异步的概念的解释,接下来我们通俗地解释一下异步:异步就是从主线程发射一

  • 详解js异步文件加载器

    我们经常会遇到这种场景,某些页面依赖第三方的插件,而这些插件比较大,不适合打包到页面的主js里(假设我们使用的是cmd的方式,js会打包成一个文件),那么这个时候我们通常会异步获取这些插件文件,并在下载完成后完成初始化的逻辑. 以图片上传为例,我们可能会用到plupload.js这个插件,那么我们会这么写: !window.plupload ? $.getScript( "/assets/plupload/plupload.full.min.js", function() { self

  • 详解Springboot对多线程的支持

    这两天看阿里的JAVA开发手册,到多线程的时候说永远不要用 new Thread()这种方式来使用多线程.确实是这样的,我一直在用线程池,到了springboot才发现他已经给我们提供了很方便的线程池机制. 本博客代码托管在github上https://github.com/gxz0422042... 一.介绍 Spring是通过任务执行器(TaskExecutor)来实现多线程和并发编程,使用ThreadPoolTaskExecutor来创建一个基于线城池的TaskExecutor.在使用线程

  • 详解JavaScript异步编程中jQuery的promise对象的作用

    Promise, 中文可以理解为愿望,代表单个操作完成的最终结果.一个Promise拥有三种状态:分别是unfulfilled(未满足的).fulfilled(满足的).failed(失败的),fulfilled状态和failed状态都可以被监听.一个愿望可以从未满足状态变为满足或者失败状态,一旦一个愿望处于满足或者失败状态,其状态将不可再变化.这种"不可改变"的特性对于一个Promise来说非常的重要,它可以避免Promise的状态监听器修改一个Promise的状态导致别的监听器的行

  • 详解Vue-Cli 异步加载数据的一些注意点

    刚开始学vue的时候没有使用脚手架,现在用脚手架写法有点不同,今天遇到的问题是使用豆瓣api异步加载数据的时候,会一直在命令行上报错,基本上错误都是xxx 未定义. 例子 <template> <div v-if="moviesData"> <!-- 正在上映的电影-北京 --> <h1>{{ moviesData.title }}</h1> </div> </template> <script&

  • 详解nodejs异步I/O和事件循环

    事件驱动模型 现在我们来看看nodejs中的事件驱动和异步I/O是如何实现的. nodejs是单线程(single thread)运行的,通过一个事件循环(event-loop)来循环取出消息队列(event-queue)中的消息进行处理,处理过程基本上就是去调用该消息对应的回调函数.消息队列就是当一个事件状态发生变化时,就将一个消息压入队列中. nodejs的时间驱动模型一般要注意下面几个点: 因为是单线程的,所以当顺序执行js文件中的代码的时候,事件循环是被暂停的. 当js文件执行完以后,事

  • 详解Java实现多线程的三种方式

    本文实例为大家分享了Java实现多线程的三种方式,供大家参考,具体内容如下 import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class Main { public static void main(String[] args) { //方法一:继承Thread int i = 0; // for(; i < 100; i++){ // System.out.println(T

随机推荐