在前面两篇Unity C#基础之 多线程的前世今生(上) 科普篇和Unity C#基础之 多线程的前世今生(中) 进阶篇中,相信大家对多线程有了一定的了解,这篇再详细的聊一聊在使用多线程中需要注意的地方~
示例工程下载Unity 2017.3.0 P4 .NET版本4.6
本篇知识点
- 异常处理
- 线程取消 CancellationTokenSource
- 多线程临时变量
- 线程安全 lock
- 语法糖 await async
异常处理
首先我们先执行下面一段代码 循环20次用Task线程执行以下Code,当执行循环 i=11 或 i=12时抛出异常
打印信息中没有 11、12的打印信息,也没有抛出异常的信息,这是因为主线程的Trycatch已经跳过
然后我们在try块中添加 Task.WaitAll(taskList.ToArray());
打印信息如下 出现抛出异常信息
然后我们去掉try块中的 Task.WaitAll(taskList.ToArray()); 在每个线程中添加Try Catch
打印结果可以捕捉到异常 所以要想捕捉到异常且不卡界面,捕捉异常最好在每个自己的线程中捕捉
相应的全部Code
private void TryCatchOnClick()
{
System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
watch.Start();
Debug.Log($"TryCatchOnClick Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
try
{
TaskFactory taskFactory = new TaskFactory();
List<Task> taskList = new List<Task>();
#region 异常处理
//多线程里的异常是会被吞掉,除非waitall
// 建议 多线程里面,是不允许异常的,也就是内部try catch,自己处理好
for (int i = 0; i < 20; i++)
{
string name = string.Format($"TryCatchOnClick{i}");
Action<object> act = t =>
{
try
{
Thread.Sleep(2000);
if (t.ToString().Equals("TryCatchOnClick11"))
{
throw new Exception(string.Format($"{t} 执行失败"));
}
if (t.ToString().Equals("TryCatchOnClick12"))
{
throw new Exception(string.Format($"{t} 执行失败"));
}
Debug.Log($"{t} 执行成功");
}
catch (Exception ex)
{
Debug.Log(ex.Message);
}
};
taskList.Add(taskFactory.StartNew(act, name));
}
//Task.WaitAll(taskList.ToArray());
#endregion
}
catch (AggregateException aex)
{
foreach (var item in aex.InnerExceptions)
{
Debug.Log(item.Message);
}
}
catch (Exception ex)
{
Debug.Log(ex.Message);
}
watch.Stop();
Debug.Log($"TryCatchOnClick End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
}
线程取消
某个线程达到预期效果后需要取消其他的线程,我们可以用 CancellationTokenSource,当然可以用一个共享的bool变量,但是CancellationTokenSource的好处是可以让没来的及启动的线程直接取消,从根本上取消启动
private void TaskCancel()
{
System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
watch.Start();
Debug.Log($"TaskCancel Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}TaskCancel");
try
{
TaskFactory taskFactory = new TaskFactory();
List<Task> taskList = new List<Task>();
//线程取消不是操作线程,而是操作信号量(共享变量,多个线程都能访问到的东西,变量/数据库的数据/硬盘数据)
//每个线程在执行的过程中,经常去查看下这个信号量,然后自己结束自己
//线程不能别人终止,只能自己干掉自己,延迟是少不了的
//CancellationTokenSource可以在cancel后,取消没有启动的任务
CancellationTokenSource cts = new CancellationTokenSource();//bool值
for (int i = 0; i < 200; i++)
{
string name = string.Format("btnThreadCore_Click{0}", i);
Action<object> act = t =>
{
try
{
Thread.Sleep(2000);
if (t.ToString().Equals("btnThreadCore_Click11"))
{
throw new Exception(string.Format("{0} 执行失败", t));
}
if (t.ToString().Equals("btnThreadCore_Click12"))
{
throw new Exception(string.Format("{0} 执行失败", t));
}
if (cts.IsCancellationRequested)//检查信号量
{
Debug.Log($"{t} 放弃执行");
return;
}
else
{
Debug.Log($"{t} 执行成功");
}
}
catch (Exception ex)
{
cts.Cancel();
Debug.Log(ex.Message);
}
};
taskList.Add(taskFactory.StartNew(act, name, cts.Token));
}
Task.WaitAll(taskList.ToArray());
}
catch (AggregateException aex)
{
foreach (var item in aex.InnerExceptions)
{
Debug.Log(item.Message);
}
}
catch (Exception ex)
{
Debug.Log(ex.Message);
}
watch.Stop();
Debug.Log($"TaskCancel End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
}
打印结果如下
多线程临时变量
这个就比较简单了,直接上Code
private void TaskTempVariable()
{
System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
watch.Start();
Debug.Log($"TaskTempVariable Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
try
{
TaskFactory taskFactory = new TaskFactory();
List<Task> taskList = new List<Task>();
////i 只有一个,真实实行的时候,已经是5了,
////k 多个k,每次是独立的k,跟i没关系
////int k;
for (int i = 0; i < 5; i++)
{
int k = i;
new Action(() =>
{
Thread.Sleep(1000);
Debug.Log($"对应的数值K:{k}");
Debug.Log($"对应的数值I:{i}");
}).BeginInvoke(null, null);
}
Task.WaitAll(taskList.ToArray());
}
catch (AggregateException aex)
{
foreach (var item in aex.InnerExceptions)
{
Debug.Log(item.Message);
}
}
catch (Exception ex)
{
Debug.Log(ex.Message);
}
watch.Stop();
Debug.Log($"TaskTempVariable End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
}
打印结果
线程安全 lock
ConcurrentDictionary多线程版字典
多线程1000次操作一个int和List
private static readonly object StaticAsyncLock = new object();
private int TotalCount = 0;//
private List<int> IntList = new List<int>(20000);
private void TaskSafe()
{
System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
watch.Start();
Debug.Log($"TaskSafe Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
try
{
TaskFactory taskFactory = new TaskFactory();
List<Task> taskList = new List<Task>();
//共有变量:都能访问局部变量/全局变量/数据库的一个值/硬盘文件
//线程内部不共享的是安全
//解决多线程冲突第一个办法:lock ,lock的方法块儿里面是单线程的,所以将整个方法Lock多线程将变得毫无意义;lock里面的代码要尽量的少
//解决多线程冲突第二个办法:没有冲突,从数据上隔离开
for (int i = 0; i < 10000; i++)
{
int TempI = i;
taskList.Add(taskFactory.StartNew(() =>
{
lock (StaticAsyncLock)//lock后的方法块,任意时刻只有一个线程可以进入语法糖 lock(StaticAsyncLock) 编译后等于 Monitor.Enter(StaticAsyncLock)
{ //这里就是单线程
this.TotalCount += 1;//多个线程同时操作,有时候操作被覆盖了
IntList.Add(TempI);
}
}));
//语法糖 lock(StaticAsyncLock) 编译后等于 Monitor.Enter(StaticAsyncLock) Monitor.Exit(StaticAsyncLock)
//检查下这个变量(引用) 有没有被lock 有就等着,没有就占用,然后进去执行,执行完了释放
//lock(this) 锁定当前实例,别的地方如果要使用这个实力里面的其他变量,则都被锁定了无法使用(不推荐这么写)
//如果每个实例想要单独的锁定 private object
//string a="123456" lock(a) string b="123456" 享元模式的内存分配,字符串值是唯一的,会锁定别的变量b
//private static readonly object StaticAsyncLock = new object();
}
Task.WaitAll(taskList.ToArray());
Debug.Log(this.TotalCount);
Debug.Log(IntList.Count);
}
catch (AggregateException aex)
{
foreach (var item in aex.InnerExceptions)
{
Debug.Log(item.Message);
}
}
catch (Exception ex)
{
Debug.Log(ex.Message);
}
watch.Stop();
Debug.Log($"TaskSafe End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
}
添加Lock锁前后打印结果
语法糖 await async (C#5.0 .NET 4.5 CLR 4.0)
如果用一句话简单概括await async,那就是:多线程版的协程
先来一个简单的示例,做一个大象装冰箱,最后咱们再详细的分析
打印结果
是不是很有趣?这种书写逻辑基本上和原来的协程一样,而且是真正的多线程,但是依据不能运行UnityEngine中的组件(例如:GameObject),下面我们详细的说一说 await async
第一个示例 在基础的方法上添加关键字 async 根据提示只有aysnc没有await 会有一个警告,跟普通方法没有区别(不得不说VS2017还是很不错的,提示、自动修补都很友好)
打印结果
第二个是示例 在第一个示例的基础上添加await关键字,这时当主线程到达await task时就返回了,继续执行方法外部的函数,可以理解为unity协程中的yeild reture,当task的线程块执行完毕后再回调 awati task后面的函数部分,这个回调的线程是不确定的:可能是主线程 可能是子线程 也可能是其他线程
/// <summary>
/// async/await
/// 不能单独await
/// await 只能放在task前面
/// 不推荐void返回值,使用Task来代替
/// Task和Task<T>能够使用await, Task.WhenAny, Task.WhenAll等方式组合使用。Async Void 不行
/// </summary>
private async void NoReturn()
{
//主线程执行
Debug.Log($"NoReturn Sleep before await,线程ID:{Thread.CurrentThread.ManagedThreadId}");
TaskFactory taskFactory = new TaskFactory();
Task task = taskFactory.StartNew(() =>
{
Debug.Log($"多线程 Sleep before,线程ID:{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(3000);
Debug.Log($"多线程 Sleep after,线程ID:{Thread.CurrentThread.ManagedThreadId}");
});
await task;//主线程到这里就返回了,执行主线程任务
//子线程执行 其实是封装成委托,在task之后成为回调(编译器功能 状态机实现)
//task.ContinueWith()
//这个回调的线程是不确定的:可能是主线程 可能是子线程 也可能是其他线程
Debug.Log($"NoReturn Sleep after await,线程ID:{Thread.CurrentThread.ManagedThreadId}");
}
打印结果
第三个示例,如果需要获取这个返回的线程怎么办呢?直接把void 换成Task
/// <summary>
/// 无返回值 async Task == async void
/// Task和Task<T>能够使用await, Task.WhenAny, Task.WhenAll等方式组合使用。Async Void 不行
/// </summary>
/// <returns></returns>
private async Task NoReturnTask()
{
//这里还是主线程的id
Debug.Log($"NoReturnTask Sleep before await,线程ID:{Thread.CurrentThread.ManagedThreadId}");
await Task.Run(() =>
{
Debug.Log($"多线程 Sleep before,线程ID:{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(9000);
Debug.Log($"多线程 Sleep after,线程ID:{Thread.CurrentThread.ManagedThreadId}");
});
Debug.Log($"NoReturnTask Sleep after await,线程ID:{Thread.CurrentThread.ManagedThreadId}");
//return new TaskFactory().StartNew(() => { }); //不能return 没有async才行
}
然后执行
打印结果
第四个示例,也是最终版的示例 返回线程+返回数值 ,获取一个Task<T> ,其中T就是返回值的类型
/// <summary>
/// 带返回值的Task
/// 要使用返回值就一定要等子线程计算完毕 卡线程
/// </summary>
/// <returns>async 就只返回long</returns>
private async Task<long> FinallyAsync()
{
Debug.Log($"SumAsync start 线程ID:{Thread.CurrentThread.ManagedThreadId}");
long result = 0;
await Task.Run(() =>
{
Debug.Log($"SumAsync await Task.Run 线程ID:{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000);
for (long i = 0; i < 999999999; i++)
{
result += i;
}
});
return result;
}
然后执行
打印结果
非await版返回值 这种主线程获取result不会卡死
/// <summary>
/// 真的返回Task 不是async
///
/// 要使用返回值就一定要等子线程计算完毕
/// </summary>
/// <returns>没有async Task</returns>
private Task<int> TaskReturn()
{
Debug.Log($"TaskReturn start 线程ID:{Thread.CurrentThread.ManagedThreadId}");
TaskFactory taskFactory = new TaskFactory();
Task<int> iResult = taskFactory.StartNew<int>(() =>
{
Thread.Sleep(3000);
Debug.Log($"TaskReturn Task.Run 线程ID:{Thread.CurrentThread.ManagedThreadId}");
return 123;
});
Debug.Log($"TaskReturn end 线程ID:{Thread.CurrentThread.ManagedThreadId}");
return iResult;
}