主要内容概要
1 多异常处理和线程取消
2 多线程的临时变量
3 线程安全和锁lock
4 await async
多线程异常
思考:
多线程中如果某一个线程异常了,就会终结当前线程;对其他的线程是没有影响的;
多线程中的异常去哪儿了? 被吞掉了。
try
{
List<Task> taskList = new List<Task>();
for (int i = 0; i < 100; i++)
{
string name = $"btnThreadCore_Click_{i}";
int k = i;
taskList.Add(Task.Run(() =>
{
if (k == 5)
{
throw new Exception($"{name} 异常了");
}
else if (k == 6)
{
throw new Exception($"{name} 异常了");
}
else if (k == 10)
{
throw new Exception($"{name} 异常了");
}
Console.WriteLine($"this is {name} Ok!");
}));
};
Task.WaitAll(taskList.ToArray());
}
catch (AggregateException aex) //可以有多个Catch 在匹配异常类型的时候,先具体,然后在寻找父类
{
foreach (var exception in aex.InnerExceptions)
{
Console.WriteLine(exception.Message);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw;
}
通过异常AggregateException 可以捕获到多线程中的异常,然后遍历InnerExceptions就可以得到具体的线程异常。
在工作中常规建议是:多线程的委托里面不想允许异常,包一层try catch,然后记录下来信息,完成一些操作。
更进一步: 在实际的工作中,往往有很多场景是如果发生异常之后,其他的线程就需要取消,不再继续往下执行;问题就是如何取消线程;
线程取消
多线程并发任务,某个失败后,希望通知别的线程,都停下来,how?
Thread.Abort() 终止线程,向当前线程抛一个异常然后终结任务:线程属于OS资源,可能不会立即停下来。
Task不能从外部终止任务,只能自己终止自己(上帝才能打败自己)
CancellationTokenSource cts = new CancellationTokenSource();// 通知式的
try
{
List<Task> taskList = new List<Task>();
for (int i = 0; i < 100; i++)
{
string name = $"btnThreadCore_Click_{i}";
int k = i;
taskList.Add(Task.Run(() =>
{
if (k == 5)
{
throw new Exception($"{name} 异常了");
}
if (!cts.IsCancellationRequested)//是否取消
{
Console.WriteLine($"this is {name} Ok!");
}
else
{
Console.WriteLine($"this is {name} Stop!");
}
}));
};
Task.WaitAll(taskList.ToArray());
}
catch (AggregateException aex) //可以有多个Catch 在匹配异常类型的时候,先具体,然后在寻找父类
{
cts.Cancel(); //执行该方法以后,IsCancellationRequested会被指定为false
foreach (var exception in aex.InnerExceptions)
{
Console.WriteLine(exception.Message);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw;
}
CancellationTokenSource这个对象有个bool属性IsCancellationRequested,对外提供一个方法Cancel()方法。可以重复Cancel,别的线程可以通过属性IsCancellationRequested来知道,是否需要撤销。
如果线程还没启动,能不能就不要启动了。
Task在Run的时候可以接受一个参数CancellationToken,可以进一步升级一下:
1.启动线程传递token
2.异常抓取
3.在Cancel时还没有启动的任务就不启动了,二十抛出异常
taskList.Add(Task.Run(() =>
{
if (k == 5)
{
throw new Exception($"{name} 异常了");
}
if (!cts.IsCancellationRequested)//是否取消
{
Console.WriteLine($"this is {name} Ok!");
}
else
{
Console.WriteLine($"this is {name} Stop!");
}
},cts.Token));
临时变量
先看一下下面两个例子:
for (int i = 0; i < 20; i++) //for 循环很快
{
Task.Run(() => // 开启线程;不会阻塞的,线程会延迟启动
{
Console.WriteLine($"btnThreadCore_Click_{i} 线程Id={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
});
}
为什么最后输出btnThreadCore_Click_20
?
这里有个临时变量的问题,线程时非阻塞的,延迟启动的;线程执行的时候,i已经是20了。
for (int i = 0; i < 20; i++)
{
int k = i; //作用域 这个k 是不是只是针对于某一次for 循环,循环20次就会有20 k
Task.Run(() =>
{
Console.WriteLine($"btnThreadCore_Click_{k} 线程Id={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
});
}
k为什么是对的呢?k是闭包里面的变量,没次循环都有一个独立的k,但是i是共享变量。事实上有5个k变量,但是只有一个i变量。
线程安全&lock
线程安全:如果你的代码在进程中有多个线程同时运行这一段,如果每次运行的结果都跟单线程运行时的结果一致,那么就是线程安全的。
private int iNumSync;
private int iNumAsync;
for (int i = 0; i < 10000; i++)
{
iNumSync++;
}
for (int i = 0; i < 10000; i++)
{
Task.Run(() =>iNumAsync++);
}
Thread.Sleep(100000);
Console.WriteLine($"iNumSync={iNumSync},iNumAsync={iNumAsync}")
可以发现单线程的结果就是稳定的10000,多线程的计算结果不是10000,且不能被预测。
上述过程就是线程不安全。
线程不安全一般出现在全局变量/共享变量/磁盘文件/静态变量/数据库的值/只要是多项去访问修改的时候,就可能会出现线程安全。
发生是因为多个线程相同操作,出现了覆盖,怎么解决呢?
Lock
- Lock是语法糖,Monitor.Enter 占据一个应用,别的线程就只能等着
- 推荐锁是
private static readonly object
, - 不能是null,也不能是string;
- 不推荐使用lock(this) 注意是一个线程还是多线程,如果外面也要用实例,就冲突了。
- 锁的作用:排他
- lock里面的代码不要太多
private static readonly object Obj_Lock = new object();
for (int i = 0; i < 100000; i++)
{
Task.Run(() =>
{
try
{
lock (Obj_Lock)//可以 避免多线程并发,如果锁住以后,其实这里跟单线程基本上没啥区别;
{
this.NumTow += 1;
}
}
catch (Exception)
{
throw;
}
});
}
调用DoTest的时候,递归作用,lock this 这里会不会死锁?
Test test = new Test();
test.DoTest();
不会死锁!!!因为是同一个线程,
public class Test
{
private int TestNum = 0;
public void DoTest()
{
lock (this) // 为了排他 锁的正常作用:应该在这儿等待,
{
Thread.Sleep(2000);
TestNum += 1;
if (DateTime.Now.Day < 13 && TestNum < 5)
{
this.DoTest();
}
else
{
Console.WriteLine("结束了");
}
}
}
}
private string Str_Lock = "ChaoqiangLock";
为什么string也不能用于锁呢?
因为string对不同的变量,如果字符串内容相同,是共享内存的,所以也是同一把锁。
线程安全集合
System.Collections.Concurrent.ConcurrentStack
System.Collections.Concurrent.ConcurrentQueue<int>
像这些都是基于线程安全。
数据分拆
避免多个线程操作同一堆数据,安全又高效率。