异常处理
我们在处理异常的时候,通常是使用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范围。
今天就到这吧,荆轲刺秦王!