• .NET异步和多线程系列(四)- 多线程异常处理、线程取消、多线程的临时变量问题、线程安全和锁lock


    本文是.NET异步和多线程系列第四章,主要介绍的是多线程异常处理、线程取消、多线程的临时变量问题、线程安全和锁lock等。

    一、多线程异常处理

    多线程里面抛出的异常,会终结当前线程,但是不会影响别的线程。那线程异常哪里去了? 被吞了

    假如想获取异常信息,这时候要怎么办呢?下面来看下其中的一种写法(不推荐):

    /// <summary>
    /// 1 多线程异常处理和线程取消
    /// 2 多线程的临时变量
    /// 3 线程安全和锁lock
    /// </summary>
    private void btnThreadCore_Click(object sender, EventArgs e)
    {
        Console.WriteLine($"****************btnThreadCore_Click Start   {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
            $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
    
        #region 多线程异常处理
    
        {
            try
            {
                List<Task> taskList = new List<Task>();
                for (int i = 0; i < 100; i++)
                {
                    string name = $"btnThreadCore_Click_{i}";
                    taskList.Add(Task.Run(() =>
                    {
                        if (name.Equals("btnThreadCore_Click_11"))
                        {
                            throw new Exception("btnThreadCore_Click_11异常");
                        }
                        else if (name.Equals("btnThreadCore_Click_12"))
                        {
                            throw new Exception("btnThreadCore_Click_12异常");
                        }
                        else if (name.Equals("btnThreadCore_Click_38"))
                        {
                            throw new Exception("btnThreadCore_Click_38异常");
                        }
                        Console.WriteLine($"This is {name}成功 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
                    }));
                }
                //多线程里面抛出的异常,会终结当前线程,但是不会影响别的线程。
                //那线程异常哪里去了? 被吞了。
                //假如我想获取异常信息,还需要通知别的线程
                Task.WaitAll(taskList.ToArray()); //1 可以捕获到线程的异常
            }
            catch (AggregateException aex) //2 需要try-catch-AggregateException
            {
                foreach (var exception in aex.InnerExceptions)
                {
                    Console.WriteLine(exception.Message);
                }
            }
            catch (Exception ex) //可以多catch  先具体再全部
            {
                Console.WriteLine(ex);
            }
    
            //线程异常后经常是需要通知别的线程,而不是等到WaitAll,问题就是要线程取消?
            //工作中常规建议:多线程的委托里面不允许异常,包一层try-catch,然后记录下来异常信息,完成需要的操作。
        }
    
        #endregion 多线程异常处理
    
        Console.WriteLine($"****************btnThreadCore_Click End   {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
            $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
    }

    上面的这种写法往往太极端了,一下子捕获了所有的异常。在真实工作中,线程异常后通常是需要通知别的线程(进行线程取消),而不是等到WaitAll。

    工作中常规建议:多线程的委托里面不允许异常,包一层try-catch,然后记录下来异常信息,完成需要的操作。具体的我们往下继续看。

    二、线程取消

    多线程并发任务,某个失败后,希望通知别的线程都停下来,要如何实现呢?

    Thread.Abort--终止线程;向当前线程抛一个异常然后终结任务;线程属于OS资源,可能不会立即停下来。非常不建议这样子去做,该方法现在也被微软给废弃了。

    既然Task不能外部终止任务,那只能自己终止自己(上帝才能打败自己),下面我们来看下具体的代码:(推荐

    #region 线程取消
    
    {
        //多线程并发任务,某个失败后,希望通知别的线程都停下来,要如何实现呢?
        //Thread.Abort--终止线程;向当前线程抛一个异常然后终结任务;线程属于OS资源,可能不会立即停下来。非常不建议这样子去做,该方法现在也被微软给废弃了。
        //Task不能外部终止任务,只能自己终止自己(上帝才能打败自己)
    
        //cts有个bool属性IsCancellationRequested 初始化是false
        //调用Cancel方法后变成true(不能再变回去),可以重复Cancel
        try
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            List<Task> taskList = new List<Task>();
            for (int i = 0; i < 50; i++)
            {
                string name = $"btnThreadCore_Click_{i}";
                taskList.Add(Task.Run(() =>
                {
                    try
                    {
                        if (!cts.IsCancellationRequested)
                            Console.WriteLine($"This is {name} 开始 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
    
                        Thread.Sleep(new Random().Next(50, 100));
    
                        if (name.Equals("btnThreadCore_Click_11"))
                        {
                            throw new Exception("btnThreadCore_Click_11异常");
                        }
                        else if (name.Equals("btnThreadCore_Click_12"))
                        {
                            throw new Exception("btnThreadCore_Click_12异常");
                        }
                        else if (name.Equals("btnThreadCore_Click_13"))
                        {
                            cts.Cancel();
                        }
                        if (!cts.IsCancellationRequested)
                        {
                            Console.WriteLine($"This is {name}成功结束 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
                        }
                        else
                        {
                            Console.WriteLine($"This is {name}中途停止 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
                            return;
                        }
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                        cts.Cancel();
                    }
                }, cts.Token));
                //加参数cts.Token目的是:在Cancel时还没有启动的任务,就不启动了。
                //但是所有没有启动的任务都会抛出一个异常cts.Token.ThrowIfCancellationRequested
            }
            //1 准备cts  2 try-catch-cancel  3 Action要随时判断IsCancellationRequested
            //尽快停止,肯定有延迟,在判断环节才会结束
    
            Task.WaitAll(taskList.ToArray());
    
            //如果线程还没启动,能不能就别启动了?加参数cts.Token
            //1 启动线程传递Token  2 异常抓取  
            //在Cancel时还没有启动的任务,就不启动了;也是抛异常,cts.Token.ThrowIfCancellationRequested
        }
        catch (AggregateException aex)
        {
            foreach (var exception in aex.InnerExceptions)
            {
                Console.WriteLine(exception.Message);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
    
    #endregion 线程取消

    CancellationTokenSource有个bool属性IsCancellationRequested,初始化是false,调用Cancel方法后变成true(不能再变回去),可以重复Cancel。cts是线程安全的

    值得一提的是,使用Task.Run启动线程的时候还传了一个cts.Token的参数,目的是:调用Cancel方法后还没有启动的任务,就不启动了,实现原理是所有没有启动的任务都会抛出一个System.Threading.Tasks.TaskCanceledException类型的异常,异常描述为“已取消一个任务”,抛出异常后任务自然也就终止了。一般情况下我们不会主动的去捕获这种异常

    那如果想看到这种异常信息的话可以通过Task.WaitAll(taskList.ToArray())加上try{...}catch (AggregateException aex){...}这种方式去捕获该类型的异常。

    PS:可以发现上面的这段代码在线程内部的地方加了一个异常捕获,工作中常规建议:多线程的委托里面不允许异常,包一层try-catch,然后记录下来异常信息,完成需要的操作。

    注意:此处的线程停止也只能说是尽快停止,肯定有延迟,在判断环节才会结束。

    三、多线程的临时变量问题

    #region 多线程的临时变量问题
    
    {
        //多线程的临时变量问题,线程是非阻塞的,延迟启动的;线程执行的时候,i已经是5了。
        for (int i = 0; i < 5; i++)
        {
            Task.Run(() =>
            {
                //此处i都是5
                Console.WriteLine($"This is btnThreadCore_Click_{i} ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
            });
        }
    
    
        //k是闭包里面的变量,每次循环都有一个独立的k
        //5个k变量  1个i变量
        for (int i = 0; i < 5; i++)
        {
            int k = i;
            Task.Run(() =>
            {
                Console.WriteLine($"This is btnThreadCore_Click_{i}_{k} ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
            });
        }
    }
    
    #endregion 多线程的临时变量问题

    运行结果如下:

    四、线程安全和锁lock

    线程安全:如果你的代码在进程中有多个线程同时运行这一段,如果每次运行的结果都跟单线程运行时的结果一致,那么就是线程安全的。

    线程安全问题一般都是有全局变量/共享变量/静态变量/硬盘文件/数据库的值,只要是多线程都能访问和修改的就有可能是非线程安全。

    非线程安全是因为多个线程相同操作,出现了覆盖,那要怎么解决?

    方案1:使用lock解决多线程冲突现在一般不推荐使用这个,会限制并发

    lock是语法糖,Monitor.Enter,占据一个引用,别的线程就只能等着。

    推荐锁是private static readonly object lockObj = new object();

    首先我们来看下lock的标准写法:

    //字段
    private static readonly object lockObj = new object();
    private int iNumSync = 0;
    private int iNumAsync = 0; //非线程安全
    private int iNumLockAsync = 0;
    private List<int> iListAsync = new List<int>();
    {
        for (int i = 0; i < 10000; i++)
        {
            this.iNumSync++; //单线程
        }
    
        for (int i = 0; i < 10000; i++)
        {
            Task.Run(() =>
            {
                this.iNumAsync++; //非线程安全
            });
        }
    
        for (int i = 0; i < 10000; i++)
        {
            Task.Run(() =>
            {
                //lock的标准写法
                //推荐锁是private static readonly object lockObj = new object();
                lock (lockObj) //任意时刻只有一个线程能进入方法块,这不就变成了单线程,限制了并发
                {
                    this.iNumLockAsync++;
                }
            });
        }
    
        for (int i = 0; i < 10000; i++)
        {
            int k = i;
            Task.Run(() => this.iListAsync.Add(k)); //非线程安全
        }
    
        Thread.Sleep(5 * 1000);
        Console.WriteLine($"iNumSync={this.iNumSync} iNumAsync={this.iNumAsync} iNumLockAsync={iNumLockAsync} listNum={this.iListAsync.Count}");
        //结果:iNumSync=1000 、 iNumAsync=1到1000之间 、 iNumLockAsync=1000 、 this.iListAsync.Count=1到1000之间
    }

    运行结果如下:

    使用lock虽然可以解决线程安全问题,但是同时也限制了并发。

    使用lock的注意点:

      A 不能是lock(null),可以编译但不能运行;

      B 不推荐lock(this),外面如果也要用实例,就冲突了;

      C 不应该是lock(string字符串),string在内存分配上是重用的,会冲突;

      D lock里面的代码不要太多,这里是单线程的;

    下面我们来看些例子:

    为什么不推荐lock(this)

    public class Test
    {
        private int iDoTestNum = 0;
        private string name = "浪子天涯";
    
        /// <summary>
        /// 锁this会和外部锁对象实例冲突
        /// </summary>
        public void DoTest()
        {
            //递归调用,lock (this)  会不会死锁? 正确答案是不会死锁!
            //这里是同一个线程,这个引用就是被这个线程所占据。
            lock (this)
            {
                Thread.Sleep(500);
                this.iDoTestNum++;
                if (this.iDoTestNum < 10)
                {
                    Console.WriteLine($"This is {this.iDoTestNum}次 {DateTime.Now.Day}");
                    this.DoTest();
                }
                else
                {
                    Console.WriteLine("28号,课程结束!!");
                }
            }
        }
    
        /// <summary>
        /// 此次锁字符串会和外部锁值相同的字符串冲突
        /// 这是因为相同的字符串会被指向同一块引用,这就相当于锁同一个引用,即同一个锁
        /// </summary>
        public void DoTestString()
        {
            //此次不会死锁
            //这里是同一个线程,这个引用就是被这个线程所占据。
            lock (this.name)
            {
                Thread.Sleep(500);
                this.iDoTestNum++;
                if (this.iDoTestNum < 10)
                {
                    Console.WriteLine($"This is {this.iDoTestNum}次 {DateTime.Now.Day}");
                    this.DoTestString();
                }
                else
                {
                    Console.WriteLine("28号,课程结束!!");
                }
            }
        }
    }
    #region 线程安全和锁lock
    
    {
        //线程安全:如果你的代码在进程中有多个线程同时运行这一段,如果每次运行的结果都跟单线程运行时的结果一致,那么就是线程安全的。
        //线程安全问题一般都是有全局变量/共享变量/静态变量/硬盘文件/数据库的值,只要是多线程都能访问和修改的就有可能是非线程安全。
        //非线程安全是因为多个线程相同操作,出现了覆盖,那要怎么解决?
    
        //1、使用lock解决多线程冲突
        //lock是语法糖,Monitor.Enter,占据一个引用,别的线程就只能等着。
        //推荐锁是private static readonly object lockObj = new object();
        //A 不能是lock(null),可以编译但不能运行;
        //B 不推荐lock(this),外面如果也要用实例,就冲突了;
        //C 不应该是lock(string字符串),string在内存分配上是重用的,会冲突;
        //D lock里面的代码不要太多,这里是单线程的;
    
        Test test = new Test();
        Task.Delay(1000).ContinueWith(t =>
        {
            lock (test) //和Test内部的lock(this)是同一个锁,故此次尽管是子线程也要排队等待
            {
                Console.WriteLine("*********lock(this) Start*********");
                Thread.Sleep(2000);
                Console.WriteLine("*********lock(this) End*********");
            }
        });
        test.DoTest();
    }
    
    #endregion 线程安全和锁lock

    运行结果如下:

    仔细观察会发现Task子线程的任务会等到test.DoTest()的任务执行完后才会执行,这是为什么呢?

    有些人可能就会有疑问了,此处锁this和锁test实例看上去应该是2把锁,互不影响才对啊,那为什么又会冲突呢?

    实际上此处的this和test是同一个实例,那么锁的当然也是同一个引用,故相当于是同一把锁。

    那又为什么不应该锁string字符串呢?

    我们在上面的例子上做一些调整如下所示:

    #region 线程安全和锁lock
    
    {
        //线程安全:如果你的代码在进程中有多个线程同时运行这一段,如果每次运行的结果都跟单线程运行时的结果一致,那么就是线程安全的。
        //线程安全问题一般都是有全局变量/共享变量/静态变量/硬盘文件/数据库的值,只要是多线程都能访问和修改的就有可能是非线程安全。
        //非线程安全是因为多个线程相同操作,出现了覆盖,那要怎么解决?
    
        //1、使用lock解决多线程冲突
        //lock是语法糖,Monitor.Enter,占据一个引用,别的线程就只能等着。
        //推荐锁是private static readonly object lockObj = new object();
        //A 不能是lock(null),可以编译但不能运行;
        //B 不推荐lock(this),外面如果也要用实例,就冲突了;
        //C 不应该是lock(string字符串),string在内存分配上是重用的,会冲突;
        //D lock里面的代码不要太多,这里是单线程的;
    
        {
            //    Test test = new Test();
            //    Task.Delay(1000).ContinueWith(t =>
            //    {
            //        lock (test) //和Test内部的lock(this)是同一个锁,故此次尽管是子线程也要排队等待
            //        {
            //            Console.WriteLine("*********lock(this) Start*********");
            //            Thread.Sleep(2000);
            //            Console.WriteLine("*********lock(this) End*********");
            //        }
            //    });
            //    test.DoTest();
        }
    
        {
            Test test = new Test();
            string student = "浪子天涯";
            Task.Delay(1000).ContinueWith(t =>
            {
                lock (student)
                {
                    Console.WriteLine("*********lock(string) Start*********");
                    Thread.Sleep(2000);
                    Console.WriteLine("*********lock(string) End*********");
                }
            });
            test.DoTestString();
        }
    }
    
    #endregion 线程安全和锁lock

    运行结果如下:

    仔细观察会发现这和lock(this)的效果是一样的,那这又是为什么呢?

    这是由于C#内存分配导致的,相同的字符串会被指向同一块引用空间,那么此处的锁this.name变量和锁student变量就相当于锁同一个引用,故相当于是同一把锁

    方案2:线程安全集合

    使用System.Collections.Concurrent.ConcurrentQueue<int>等相关操作,System.Collections.Concurrent命名空间下的相关操作是线程安全的。

    方案3:数据分拆,避免多线程操作同一个数据,又安全又高效推荐

    在真实工作中遇到线程不安全的情况,如果有办法使用数据分拆来解决则推荐使用数据分拆,数据分拆无法解决的时候再考虑使用锁。

    Demo源码:

    链接:https://pan.baidu.com/s/1Eaet92HhGoK9sHjXhz_VsA 
    提取码:7st0

    此文由博主精心撰写转载请保留此原文链接:https://www.cnblogs.com/xyh9039/p/13592042.html

    版权声明:如有雷同纯属巧合,如有侵权请及时联系本人修改,谢谢!!!

  • 相关阅读:
    elasticsearch
    CentOS6.9安装Logstash
    CentOS6.9安装Filebeat监控Nginx的访问日志发送到Kafka
    openresty capture
    CentOS6.9安装socat
    CentOS挂Windows的NFS备忘
    openCV 备忘
    rabbitmq更换数据文件和日志文件的存放位置
    根据某个文件或文件夹自制rpm包
    checkinstall打包工具使用
  • 原文地址:https://www.cnblogs.com/xyh9039/p/13592042.html
Copyright © 2020-2023  润新知