Unity C#基础之 多线程的前世今生(下) 扩展篇

简介: 在前面两篇Unity C#基础之 多线程的前世今生(上) 科普篇和Unity C#基础之 多线程的前世今生(中) 进阶篇中,相信大家对多线程有了一定的了解,这篇再详细的聊一聊在使用多线程中需要注意的地方~示例工程下载Unity 2017.

在前面两篇Unity C#基础之 多线程的前世今生(上) 科普篇Unity C#基础之 多线程的前世今生(中) 进阶篇中,相信大家对多线程有了一定的了解,这篇再详细的聊一聊在使用多线程中需要注意的地方~

示例工程下载Unity 2017.3.0 P4 .NET版本4.6

本篇知识点

  • 异常处理
  • 线程取消 CancellationTokenSource
  • 多线程临时变量
  • 线程安全 lock
  • 语法糖 await async

异常处理

首先我们先执行下面一段代码 循环20次用Task线程执行以下Code,当执行循环 i=11i=12时抛出异常

img_bff22709381b53e2837a681ac1b99ddb.png

打印信息中没有 11、12的打印信息,也没有抛出异常的信息,这是因为主线程的Trycatch已经跳过
img_bd5b10e59a2513cb2c5b5b4970559b1a.png

然后我们在try块中添加 Task.WaitAll(taskList.ToArray());

img_e2bf3463f406c1df001f186d7cc2c4d7.png
打印信息如下 出现抛出异常信息
img_e59ba106e1a9bc9c34fd3afd1a230de5.png

img_2b64273f54b5fbc04ee1b080bd095a6b.png
然后我们去掉try块中的 Task.WaitAll(taskList.ToArray()); 在每个线程中添加Try Catch
img_c7e39b141af0b4a9e4e033e438deb217.png

打印结果可以捕捉到异常 所以要想捕捉到异常且不卡界面,捕捉异常最好在每个自己的线程中捕捉

img_95eabd2287c6a4b91efd8e15b3b6c3c4.png

相应的全部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")}");
    }
打印结果如下
img_c91cbaa8ebe918660895435ab41d6b61.png

img_d48bf0c44100318a43a792a4250ff829.png

多线程临时变量

这个就比较简单了,直接上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")}");
    }

打印结果

img_6fafc4a284596921a57e5a2c188ee25e.png

线程安全 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锁前后打印结果

img_91599f9bef1578153ea90075c5855d1e.png

img_4918194c02e04fec0fee785dedde272a.png

语法糖 await async (C#5.0 .NET 4.5 CLR 4.0)

如果用一句话简单概括await async,那就是:多线程版的协程

先来一个简单的示例,做一个大象装冰箱,最后咱们再详细的分析

img_85e12026dbd6337f3e81d9d6c38ffa24.png

img_4be15c23e036c19a227b89ef3c42cfa3.png
img_4c385651660949dd68779d71aa9a2b23.png

打印结果

img_fa00c5b99b66ed93d353571ecffe1ae6.png

是不是很有趣?这种书写逻辑基本上和原来的协程一样,而且是真正的多线程,但是依据不能运行UnityEngine中的组件(例如:GameObject),下面我们详细的说一说 await async

第一个示例 在基础的方法上添加关键字 async 根据提示只有aysnc没有await 会有一个警告,跟普通方法没有区别(不得不说VS2017还是很不错的,提示、自动修补都很友好)

img_641d02fe314fb5dd697ca4d14b2a4fa9.png

打印结果
img_f4ad85653406f78fd8222b912b3df70d.png

第二个是示例 在第一个示例的基础上添加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}");
    }

打印结果
img_c5d36978e19351cb5359c349c91f9c0e.png

第三个示例,如果需要获取这个返回的线程怎么办呢?直接把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才行
    }
然后执行
img_7fca65416a936099a820379f5f24cff8.png

打印结果

img_4b297f334a6dc22cbc2db72861897ada.png

第四个示例,也是最终版的示例 返回线程+返回数值 ,获取一个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;
    }
然后执行
img_760d8d820b27a6c96cf2a42ca4e5699a.png

打印结果

img_20cb1867652472d9e3472845a9ad642f.png

非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;
    }
执行函数
img_ada8e59ca8412e38e4f850cb025d8253.png
打印结果
img_0cd2848385ea4e8f6da5bd394c1d18a0.png

以上就是await async 的相关内容,有需要补充的东西欢迎留言

相关文章
|
4月前
|
数据采集 XML JavaScript
C# 中 ScrapySharp 的多线程下载策略
C# 中 ScrapySharp 的多线程下载策略
|
1月前
|
设计模式 C# 图形学
Unity 游戏引擎 C# 编程:一分钟浅谈
本文介绍了在 Unity 游戏开发中使用 C# 的基础知识和常见问题。从 `MonoBehavior` 类的基础用法,到变量和属性的管理,再到空引用异常、资源管理和性能优化等常见问题的解决方法。文章还探讨了单例模式、事件系统和数据持久化等高级话题,旨在帮助开发者避免常见错误,提升游戏开发效率。
51 4
|
2月前
|
Java 应用服务中间件
面对海量网络请求,Tomcat线程池如何进行扩展?
【10月更文挑战第4天】本文详细探讨了Tomcat线程池相较于标准Java实用工具包(JUC)线程池的关键改进。首先,Tomcat线程池在启动时即预先创建全部核心线程,以应对启动初期的高并发请求。其次,通过重写阻塞队列的入队逻辑,Tomcat能够在任务数超过当前线程数但未达最大线程数时,及时创建非核心线程,而非等到队列满才行动。此外,Tomcat还引入了在拒绝策略触发后重新尝试入队的机制,以提高吞吐量。这些优化使得Tomcat线程池更适应IO密集型任务,有效提升了性能。
面对海量网络请求,Tomcat线程池如何进行扩展?
|
1月前
|
C# Python
使用wxpython开发跨平台桌面应用,对wxpython控件实现类似C#扩展函数处理的探究
【10月更文挑战第30天】使用 `wxPython` 开发跨平台桌面应用时,可以通过创建辅助类来模拟 C# 扩展函数的功能。具体步骤包括:1. 创建辅助类 `WxWidgetHelpers`;2. 在该类中定义静态方法,如 `set_button_color`;3. 在应用中调用这些方法。这种方法提高了代码的可读性和可维护性,无需修改 `wxPython` 库即可为控件添加自定义功能。但需要注意显式调用方法和避免命名冲突。
|
3月前
|
设计模式 存储 人工智能
深度解析Unity游戏开发:从零构建可扩展与可维护的游戏架构,让你的游戏项目在模块化设计、脚本对象运用及状态模式处理中焕发新生,实现高效迭代与团队协作的完美平衡之路
【9月更文挑战第1天】游戏开发中的架构设计是项目成功的关键。良好的架构能提升开发效率并确保项目的长期可维护性和可扩展性。在使用Unity引擎时,合理的架构尤为重要。本文探讨了如何在Unity中实现可扩展且易维护的游戏架构,包括模块化设计、使用脚本对象管理数据、应用设计模式(如状态模式)及采用MVC/MVVM架构模式。通过这些方法,可以显著提高开发效率和游戏质量。例如,模块化设计将游戏拆分为独立模块。
223 3
|
4月前
|
开发者 图形学 开发工具
Unity编辑器神级扩展攻略:从批量操作到定制Inspector界面,手把手教你编写高效开发工具,解锁编辑器隐藏潜能
【8月更文挑战第31天】Unity是一款强大的游戏开发引擎,支持多平台发布与高度可定制的编辑器环境。通过自定义编辑器工具,开发者能显著提升工作效率。本文介绍如何使用C#脚本扩展Unity编辑器功能,包括批量调整游戏对象位置、创建自定义Inspector界面及项目统计窗口等实用工具,并提供具体示例代码。理解并应用这些技巧,可大幅优化开发流程,提高生产力。
441 1
|
3月前
|
安全 数据库连接 API
C#一分钟浅谈:多线程编程入门
在现代软件开发中,多线程编程对于提升程序响应性和执行效率至关重要。本文从基础概念入手,详细探讨了C#中的多线程技术,包括线程创建、管理及常见问题的解决策略,如线程安全、死锁和资源泄露等,并通过具体示例帮助读者理解和应用这些技巧,适合初学者快速掌握C#多线程编程。
88 0
|
3月前
|
图形学 C++ C#
Unity插件开发全攻略:从零起步教你用C++扩展游戏功能,解锁Unity新玩法的详细步骤与实战技巧大公开
【8月更文挑战第31天】Unity 是一款功能强大的游戏开发引擎,支持多平台发布并拥有丰富的插件生态系统。本文介绍 Unity 插件开发基础,帮助读者从零开始编写自定义插件以扩展其功能。插件通常用 C++ 编写,通过 Mono C# 运行时调用,需在不同平台上编译。文中详细讲解了开发环境搭建、简单插件编写及在 Unity 中调用的方法,包括创建 C# 封装脚本和处理跨平台问题,助力开发者提升游戏开发效率。
302 0
|
4月前
|
图形学 C# 开发者
全面掌握Unity游戏开发核心技术:C#脚本编程从入门到精通——详解生命周期方法、事件处理与面向对象设计,助你打造高效稳定的互动娱乐体验
【8月更文挑战第31天】Unity 是一款强大的游戏开发平台,支持多种编程语言,其中 C# 最为常用。本文介绍 C# 在 Unity 中的应用,涵盖脚本生命周期、常用函数、事件处理及面向对象编程等核心概念。通过具体示例,展示如何编写有效的 C# 脚本,包括 Start、Update 和 LateUpdate 等生命周期方法,以及碰撞检测和类继承等高级技巧,帮助开发者掌握 Unity 脚本编程基础,提升游戏开发效率。
107 0