iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >详解C#异步多线程使用中的常见问题
  • 134
分享到

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

2024-04-02 19:04:59 134人浏览 安东尼
摘要

目录异常处理线程取消临时变量线程安全异常处理 小伙伴有没有想过,多线程的异常怎么处理,同步方法内的异常处理,想必都非常非常熟悉了。那多线程是什么样的呢,接着我讲解多线程的异常处理 首

异常处理

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

首先,我们定义个任务列表,当 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#异步多线程内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

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

本文链接: https://www.lsjlt.com/news/161635.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 详解C#异步多线程使用中的常见问题
    目录异常处理线程取消临时变量线程安全异常处理 小伙伴有没有想过,多线程的异常怎么处理,同步方法内的异常处理,想必都非常非常熟悉了。那多线程是什么样的呢,接着我讲解多线程的异常处理 首...
    99+
    2024-04-02
  • C#异步多线程使用中的常见问题有哪些
    本篇内容主要讲解“C#异步多线程使用中的常见问题有哪些”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“C#异步多线程使用中的常见问题有哪些”吧!异常处理小伙伴有没有想过,多线程的异常怎么处理,同步...
    99+
    2023-06-22
  • C++中的多线程同步问题详解
    C++中的多线程同步问题详解在并发编程中,多线程同步是一个重要的问题。当多个线程同时访问共享资源时,会引发各种问题,如竞态条件(Race Condition)、死锁(Deadlock)和活锁(Livock),这些问题都会导致程序的不确定性和...
    99+
    2023-10-22
    C++ 多线程 同步问题
  • C#中常见的线程同步问题及解决方法
    C#中常见的线程同步问题及解决方法引言:在多线程编程中,线程同步是一个关键的概念。当多个线程同时访问共享资源时,会导致数据不一致或出现竞态条件等问题。本文将介绍C#中常见的线程同步问题,并提供相应的解决方法和示例代码。一、不正确的数据共享当...
    99+
    2023-10-22
    线程同步问题 C#中的线程同步
  • 基于多线程并发的常见问题(详解)
    一 概述1.volatile保证共享数据一旦被修改就会立即同步到共享内存(堆或者方法区)中。2.线程访问堆中数据的过程线程在栈中建立一个数据的副本,修改完毕后将数据同步到堆中。3.指令重排为了提高执行效率,CPU会将没有依赖关系的指令重新排...
    99+
    2023-05-31
    多线程 并发 线程并发
  • 如何解决 C++ 多线程编程中常见的死锁问题?
    如何解决 c++++ 多线程编程中的常见死锁问题?避免死锁的技术:加锁顺序:始终以相同的顺序获取锁。死锁检测:使用算法检测并解决死锁。超时:为锁设置超时值,防止线程无限期等待。优先级反转...
    99+
    2024-05-13
    多线程编程 死锁 c++
  • Java 线程池常见问题详解
    线程池是一个预定义线程集合,可按需提供给应用程序使用。它通过管理线程的创建和销毁,简化了线程处理,提高了应用程序的性能和可伸缩性。 为什么使用线程池? 使用线程池有以下好处: 减少线程创建和销毁的开销,提高性能。 限制并发线程数,防止系...
    99+
    2024-03-13
    线程池
  • PHP 多线程和异步编程的常见陷阱?
    常见的多线程和异步编程陷阱包括共享状态、死锁、超时、资源泄漏和调试困难。在多线程应用程序中,共享状态必须受到保护,死锁可以通过超时机制避免,超时的适当设置也很重要。资源泄漏可以通过正确释...
    99+
    2024-05-06
    php 多线程 同步机制
  • C++ 多线程编程带来的常见问题是什么?
    多线程编程中常见问题包括:数据竞争(共享数据同时被访问和修改)、死锁(线程相互等待)、代码抽象(管理同步细节的复杂性)、调试难度(非确定性导致问题难以查明)。解决这些问题的方法包括使用同...
    99+
    2024-05-13
    c++ 多线程 同步机制
  • C++中常见的异常处理问题解决方法
    C++中常见的异常处理问题解决方法,需要具体代码示例引言:在编写C++程序时,时常会遇到程序出现异常的情况,如除数为0、数组越界、空指针访问等等。这些异常会导致程序的崩溃或者产生不可预测的结果,为了增强程序的稳定性和可靠性,我们需要使用异常...
    99+
    2023-10-22
    解决方法 异常处理 C++异常
  • C++中常见的代码重用问题详解
    C++中常见的代码重用问题详解在软件开发中,代码重用是提高开发效率和代码可维护性的重要方法之一。C++作为一种广泛使用的编程语言,提供了多种重用代码的机制,如函数、类、模板等。然而,代码重用并不总是简单和直接的,往往会遇到一些常见的问题。本...
    99+
    2023-10-22
    继承 (Inheritance) 多态 (Polymorphism) 代码重用问题 C++中常见的编程关键词有: (
  • C++中常见的代码复用问题详解
    C++中常见的代码复用问题详解代码复用是软件开发中的重要概念,它可以提高开发效率和代码质量。然而,在C++语言中,存在一些常见的代码复用问题,如代码重复、可维护性差等。本文将详细介绍这些问题,并给出具体的代码示例,帮助读者更好地理解和解决这...
    99+
    2023-10-22
    接口 (interface) 继承 (Inheritance) 组合 (Composition)
  • Python使用asyncio异步时的常见问题总结
    目录1. 如何停止任务?2. 如何等待任务完成?3. 如何从任务中获取返回值?4. 如何在后台运行任务?5. 如何等待所有后台任务?1. 如何停止任务? 我们可以通过 asyncio...
    99+
    2023-05-16
    Python asyncio异步常见问题 Python asyncio异步 Python asyncio
  • spring boot使用@Async注解解决异步多线程入库的问题
    目录前言项目实况介绍第一种方式第二种方式这里有个坑!这里有两个坑!总结前言 在开发过程中,我们会遇到很多使用线程池的业务场景,例如定时任务使用的就是ScheduledThreadPo...
    99+
    2024-04-02
  • C++中常见的代码复用问题的详解
    C++中常见的代码复用问题的详解在C++编程中,代码复用是一种重要的技术,可以提高代码的可维护性和重用性。然而,在实践中,我们常常会遇到一些代码复用的问题。本文将探讨一些常见的问题,并提供具体代码示例。一、继承带来的问题继承是C++中实现代...
    99+
    2023-10-22
    代码复用 关键词:C++ 问题详解
  • ASP异步编程的常见问题及解决方案?
    ASP异步编程的常见问题及解决方案 随着互联网技术的不断发展,网页的交互性和实时性要求也越来越高。而ASP异步编程技术可以帮助开发者解决这些问题。但是,ASP异步编程也会遇到一些常见问题,本文将结合实例介绍这些问题及其解决方案。 一、什么是...
    99+
    2023-09-12
    异步编程 面试 windows
  • 常见的Java多线程问题有哪些
    这篇文章主要介绍常见的Java多线程问题有哪些,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!1、多线程有什么用?一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了,还管它有什么用?在我看来,这个回答更扯淡。所谓...
    99+
    2023-06-02
  • C#异步多线程ThreadPool怎么使用
    这篇文章主要讲解了“C#异步多线程ThreadPool怎么使用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“C#异步多线程ThreadPool怎么使用”吧!启动线程池线程ThreadPool...
    99+
    2023-06-25
  • C++中的多线程同步问题及解决方法
    C++中的多线程同步问题及解决方法多线程编程是提高程序性能和效率的一种方式,但同时也带来了一系列的同步问题。在多线程编程中,多个线程可能会同时访问和修改共享的数据资源,这可能导致数据的竞争条件、死锁、饥饿等问题。为了避免这些问题,我们需要使...
    99+
    2023-10-22
    多线程 (Multithreading) 同步 (synchronization) 解决方法 (Solution)
  • C++中常见的空指针异常问题解决方案
    C++中常见的空指针异常问题解决方案引言:在C++编程中,空指针异常是一种常见的错误类型。当程序试图访问指向空地址的指针时,就会导致空指针异常的发生。在大型项目中,空指针异常可能会导致程序崩溃或产生不可预期的行为。因此,开发人员需要了解如何...
    99+
    2023-10-22
    C++ 解决方案 空指针异常
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作