C#多线程那点事儿

异常处理

我们在处理异常的时候,通常是使用Try Catch将其代码段进行包裹,期间发生异常的情况下,我们都能将异常捕获到,进入catch里面执行相应的代码,于是乎,我在多线程里面也这样做了一下,发现事情好像没有那么简单。

 Console.WriteLine($"--------------开始{Thread.CurrentThread.ManagedThreadId.ToString("00")}----------------");
            try
            {
                List<Task> tasks = new List<Task>();
                #region 异常处理
                for (int i = 0; i < 20; i++)
                {
                    Action<object> action = t =>
                    {
                        Thread.Sleep(2000);
                        if (t.ToString().Equals("11")|| t.ToString().Equals("12"))
                        {
                            throw new Exception(string.Format($"{t} 执行失败"));
                        }
                        Console.WriteLine($"{t}执行成功");
                    };
                    tasks.Add(taskFactory.StartNew(action, i));
                }
                #endregion
            }
            catch (AggregateException aex)
            {
                foreach (var item in aex.InnerExceptions)
                {
                    Console.WriteLine(item.Message);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            Console.WriteLine($"--------------结束{Thread.CurrentThread.ManagedThreadId.ToString("00")}----------------");

按照我的认知呢,异常将会被捕获,从而打印出哪个执行失败,但是运行结果是

11和12没有被捕获,那我们可以看出来,其实主线程早已经结束,我是这样理解的,这时里面的任务还没执行完,但是try  catch已经检测不到程序里面的异常了,这时候如果在try 里面加上一个Task.WaitAll(tasks.ToArray());,异常就可以捕获到了,因为主线程被阻塞了,这时候try catch还是可以检测到try里面代码的异常的,但是我们大多数时候也不大会去用WaitAll,那就只能在多线程action里面进行异常处理了,这样才能避免异常被吞掉。

线程取消

在某些业务场景中,我们希望当多线程中有一个线程发生错误之后,要把所有线程给停下来,这时候我们就要用到线程取消了,由于线程是在OS中的,我们在外部无法停止,那么可以使用CancellationTokenSource来实现,在每次线程里的action执行时检测一下这个信号量,它可以指示是否还可继续执行,其实也就是个bool值,下面看一下代码

try
            {
                List<Task> tasks = new List<Task>();
                TaskFactory taskFactory = new TaskFactory();
#region 线程取消
                CancellationTokenSource source = new CancellationTokenSource();
                for (int i = 0; i < 40; i++)
                {
                    Action<object> action = t =>
                    {
                        try
                        {
                            Thread.Sleep(2000);
                            if (t.ToString().Equals("11"))
                            {
                                throw new Exception($"{t}执行失败");
                            }
                            if (source.IsCancellationRequested)//检查信号量
                            {
                                Console.WriteLine($"{t}放弃执行");
                                return;
                            }
                            else
                            {
                                Console.WriteLine($"{t}执行成功");
                            }
                        }
                        catch (Exception ex)
                        {
                            source.Cancel();
                            Console.WriteLine(ex.Message);
                        }
                    };
                    tasks.Add(taskFactory.StartNew(action, i, source.Token));
                }
                Task.WaitAll(tasks.ToArray());
                #endregion
            }
            catch (AggregateException aex)
            {
                foreach (var item in aex.InnerExceptions)
                {
                    Console.WriteLine(item.Message);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }

执行到11的时候失败了,这时,18到27这几个任务是已经启动了的,然后检测了一下发现取消了,那27之后的呢,还没有开始启动则是报了异常,打印了一个“已取消一个任务”,

这个异常则是taskFactory.StartNew 方法的第三个参数检测到的,如果不加第三个参数,程序还是会再启动线程,打印至39,加上这个参数,相当于它告诉了程序说已经发生异常了,不要在启动线程了。

多线程临时变量

先看一段代码

for (int i = 0; i < 5; i++)
                {
                    int k = i;
                    new Action(() =>
                    {
                        Thread.Sleep(100);
                        Console.WriteLine($"k={k} i={i}");
                    }).BeginInvoke(null, null);
                }

猜一下结果是什么,原来我认为k和i的值是一样的,但是并不是

为什么会这样呢

这里i始终是一个i,但是k是每次都new了一个的,k是5个k,那么为什么i是5呢,在程序执行的过程中,这一个for循环执行是非常快的,action在等待打印的时候或者还没有开始执行的时候,i已经变成了5了,然后在打印的时候i一直是5,而k则把每次循环的i保存了下来,所以就是现在呈现的结果了

线程安全

先来一段代码 

 int TotalCount = 0;
                List<int> listIn = new List<int>();
                for (int i = 0; i < 10000; i++)
                {
                    tasks.Add(taskFactory.StartNew(() =>
                    {
                        TotalCount++;
                        listIn.Add(i);
                    }));
                }
                Task.WaitAll(tasks.ToArray());
                Console.WriteLine(TotalCount);
                Console.WriteLine(listIn.Count);

猜一猜打印了多少

怎么不是1000呢,少了几个怎么回事,原来TotalCount和LisIn是全局变量,每一个Action都可以访问到,在多线程中,可能同时有两个action给TotalCount赋值,前面这个还没有附上值,然后后面紧跟着把值给冲掉了,就导致了会少了一些数据,那么怎么解决这个问题呢

我们可以使用lock将变量给锁住,每次只能有一个线程进入

那么就是这样

private static readonly object lockObj = new object();//private  防止外面也去lock   static 全场唯一  readonly不要改动  object表示引用
tasks.Add(taskFactory.StartNew(() =>
                    {
                        lock (lockObj)
                        {
                            TotalCount++;
                            listIn.Add(i);
                        }
                    }));

Lock相当于//Monitor.Enter(lockObj);   

           //Monitor.Exit(lockObj);

这样是可以解决问题,但是牺牲了性能,所以要尽量缩小lock范围。

今天就到这吧,荆轲刺秦王!

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值