【C#】并行编程实战:编写并行和异步代码的单元测试用例

        本章将介绍如何编写并行和异步代码的单元测试用例。编写单元测试是大型项目的重要组成部分,是代码健壮可靠、易于维护的必然要求。不过本章书上的代码与 Unity 基本没什么关系,也不适用。但是单元测试在 Unity 中依然是必要的,这里我们基于 Unity 来讨论单元测试。

        本教程学习工程:魔术师Dix / HandsOnParallelProgramming · GitCode


1、安全地启动 Task

        根据前面的学习我们了解到,直接使用 Task.Run 是不会抛出异常的。如果直接在每一个方法里都写上 Try-Catch 又过于繁琐,所以使用通用的 Task 安全写法更为方便。

        之前章节已经介绍过,Try-Catch 是不能跨线程的,所以需要有一个线程等待、收集所有的错误并抛出,而这个等待又不能在主线程等待避免卡死:

        public static void RunTaskSafe(Func<Task> function)
        {
            Task.Run(() =>
            {
                try
                {
                    var task = Task.Run(function);
                    task.Wait();
                }
                catch (AggregateException ex)
                {
                    Debug.LogError(ex.Message);
                    Debug.LogError(ex.InnerException);
                }
            });
        }

        如上示例代码,就能正确抛出错误,且不会卡死主线程:

 

2、Task 的性能测试

        通常做法,就是使用 Task.Wait ,然后就直接可以计时看时间了:

Stopwatch stopwatch = Stopwatch.StartNew();
stopwatch.Start();

var task = Task.Run(function);
task.Wait();

stopwatch.Stop();
Debug.Log($"【{function.Method.Name} 】耗时:{stopwatch.Elapsed.TotalMilliseconds}");

        如上所示,就能正常工作打印出函数耗时。

        但是显然这种打印的耗时十分不方便,而且其本身也有 GC 和性能开销,最多在关键节点调试时查看一下性能,实际上不能大规模使用。

        最好的方案自然是在 Unity 的 Profiler 窗口中能查看到性能报告。

https://docs.unity3d.com/cn/2022.2/ScriptReference/Profiling.Profiler.BeginThreadProfiling.htmlicon-default.png?t=N6B9https://docs.unity3d.com/cn/2022.2/ScriptReference/Profiling.Profiler.BeginThreadProfiling.html        实际上是可以办到的。在 Unity 2017.3.0 之后的版本中,Unity 添加了适用于多线程性能分析接口:Profiling.BeginThreadProfiling 。因此我们将启动 Task 的写法改造如下:

        public static Task Run(Action action)
        {
            if (action == null)
                return Task.CompletedTask;

            var task = Task.Run(() =>
            {
                try
                {
#if TASK_PROFILE
                    var method = action.Method;
                    if (method == null)
                        return;

                    Profiler.BeginThreadProfiling("GYTask", $"GYTask_{Task.CurrentId}");
                    Profiler.BeginSample(method.GetMethodName());
#endif
                    //在子线程运行全部任务,并等待完成以收集错误
                    var task = Task.Run(action);
                    task.Wait();

#if TASK_PROFILE
                    Profiler.EndSample();
                    Profiler.EndThreadProfiling();
#endif
                }
                catch (AggregateException ex)
                {
                    UnityEngine.Debug.LogError(ex.Message);
                    UnityEngine.Debug.LogError(ex.InnerException);
                }
            });

            return task;
        }

        之后我们执行一个耗时函数,就能在 Profiler 中看到效果了:

         当然,我们在主线程中是看不到效果的,Timeline 中可以看到这部分代码并行了。同样,我们也可以在 Hierarchy 中选择对应线程查看运行情况:

         我认为这个比 VS 提供的那些工具好多了 !

3、一些性能开销问题

        有了性能测试工具,我们可以对一些 TPL 的性能情况进行测试,这里我选了几个我比较关注的地方:

3.1、创建 Task 的开销

        这个比较好测试,直接在主线程就能测出来:

         可见,主要是 GC 的问题:

  • Task.Run 本身的构造会产生 GC (~ 0.8 KB)。

  • 匿名函数的构造本身也会产生 GC (~ 0.12 KB)。

        从耗时来看,单次创建一个 Task (以安全方式)耗时约为 0.1 ms (上图中 10 次启动耗时约 1 ms)。总结下来就是:启动一个 Task,耗时 0.1 ms,造成 1 KB GC

        创建匿名函数的 GC 和耗时可能可以通过某些手段消耗掉,但本章节不做探讨,后续研究如果有方案可以使用。

3.2、Task.Delay 的开销

        从上面的性能测试示例看得出来,Task.Delay 其实就是一个特殊函数占满了线程而已,没有 GC ,但是耗时看得出来是每帧吃满了的。

        可能单纯地 Delay 对 CPU 运行压力不会有很大影响,但是在性能测试上能看得出来一个显著峰值。

3.3、信号灯的开销

        信号灯和 Delay 一样,能在 Profiler 上展示出开销:

         毕竟这个和 Delay 都可以理解为将子线程阻塞了。

        当然这个测试其实和我的性能测试代码写法有关系,我是计算的等待任务完成的时间,因此只要这个方法还在运行,就一定会产生开销。


4、本章小节

        这一章介绍了 TPL 在 Unity 内的通用写法示例,解决了多线程的两大痛点:不能抛出错误、无法查看性能开销。个人觉得还是比较有用的,代码可以用作参考。

        当然,要适配所有 API 还是需要各种后续补充。

        本教程学习工程:魔术师Dix / HandsOnParallelProgramming · GitCode

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值