找回密码
 立即注册
查看: 387|回复: 1

[Unity 3d] 老瓶新酒,看我基于PlayerLoopSystem的Loom新写法

[复制链接]
发表于 2022-4-14 20:58 | 显示全部楼层 |阅读模式
Loom :在 Unity 多线程编程中实现线程间的数据同步,避免非主线程直接操作 Unity 对象。
在本文,笔者将使用 UnityEngine.LowLevel 命名空间下的 PlayerLoop 提供的 API 来重写 Loom 细节 。
前言:

在多线程异步编程中,非 UI 线程不得操作 UI 组件 (Unity中则是不得操作继承 UnityEngin.Object的组件),因此,便需要一个同步上下文的工具在各个“平行”的线程中来回穿插,传递线程执行的结果。

于是,我和 Loom 相遇了,这是一个久远而又美妙的相遇,虽 N 久不用,犹念念不忘。
Loom 译为织机 ,用在线程间数据同步,形如快速穿梭在众多平行线之中的梭子,意境恰如其名~

前段时间写 Security-Camera-Toolkit-For-Unity 时有用到 async /await 语法糖,需要用到线程间数据同步,便写了一个,名曰:TaskSync ,译为:任务同步器

临近行文,笔者兴起将 TaskSync 重命名为 Loom,于是本文标题也顺其自然的引入了:老瓶新酒 的说法,下面就看看笔者是如何将 “新酒” 装入如此经典的 “老瓶” 之中的...
实现:

    使用属性:RuntimeInitializeOnLoadMethod 在场景载入前安装本工具。
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]static void Install(){}
    使用方法:PlayerLoop.GetCurrentPlayerLoop()  获得 PlayerLoopSystem
var playerloop = PlayerLoop.GetCurrentPlayerLoop();
    用户自定义一个 PlayerLoopSystem 并插入到第二步中获取的 PlayerLoopSystem 中
var loop = new PlayerLoopSystem{   type = typeof(Loom),   updateDelegate = Update};//1. 找到 Update Loop Systemint index = Array.FindIndex(playerloop.subSystemList, v => v.type == typeof(UnityEngine.PlayerLoop.Update));//2.  将咱们的 loop 插入到 Update loop 中var updateloop = playerloop.subSystemList[index];var temp = updateloop.subSystemList.ToList();temp.Add(loop);updateloop.subSystemList = temp.ToArray();playerloop.subSystemList[index] = updateloop;
    使用方法:PlayerLoop.SetPlayerLoop() 将编辑后的 PlayerLoopSystem 设置回 Unity 引擎。
//3. 设置自定义的 Loop 到 Unity 引擎PlayerLoop.SetPlayerLoop(playerloop);代码:
Talk is cheap ,show me the code.
// Copyright (c) https://github.com/Bian-Sh// Licensed under the MIT License.using System;using System.Collections.Concurrent;using System.Linq;using System.Threading;#if UNITY_EDITORusing UnityEditor;#endifusing UnityEngine;using UnityEngine.LowLevel;namespace zFramework.Media.Internal{    /// <summary>    /// 任务同步器:在主线程中执行 Action 委托    /// <br>原名 TaskSync,但是觉得 Loom(织布机)更有意境</br>    /// </summary>    public static class Loom    {        static SynchronizationContext context;        static readonly ConcurrentQueue<Action> tasks = new ConcurrentQueue<Action>();        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]        static void Install()        {            context = SynchronizationContext.Current;            #region 使用 PlayerLoop 在 Unity 主线程的 Update 中更新本任务同步器            var playerloop = PlayerLoop.GetCurrentPlayerLoop();            var loop = new PlayerLoopSystem            {                type = typeof(Loom),                updateDelegate = Update            };            //1. 找到 Update Loop System            int index = Array.FindIndex(playerloop.subSystemList, v => v.type == typeof(UnityEngine.PlayerLoop.Update));            //2.  将咱们的 loop 插入到 Update loop 中            var updateloop = playerloop.subSystemList[index];            var temp = updateloop.subSystemList.ToList();            temp.Add(loop);            updateloop.subSystemList = temp.ToArray();            playerloop.subSystemList[index] = updateloop;            //3. 设置自定义的 Loop 到 Unity 引擎            PlayerLoop.SetPlayerLoop(playerloop);#if UNITY_EDITOR            //4. 已知:编辑器停止 Play 我们自己插入的 loop 依旧会触发,进入或退出Play 模式先清空 tasks            EditorApplication.playModeStateChanged -= EditorApplication_playModeStateChanged;            EditorApplication.playModeStateChanged += EditorApplication_playModeStateChanged;            static void EditorApplication_playModeStateChanged(PlayModeStateChange obj)            {                if (obj == PlayModeStateChange.ExitingEditMode ||                      obj == PlayModeStateChange.ExitingPlayMode)                {                    //清空任务列表                    while (tasks.TryDequeue(out _)) { }                }            }#endif            #endregion        }#if UNITY_EDITOR        //5. 确保编辑器下推送的事件也能被执行        [InitializeOnLoadMethod]        static void EditorForceUpdate()        {            Install();            EditorApplication.update -= ForceEditorPlayerLoopUpdate;            EditorApplication.update += ForceEditorPlayerLoopUpdate;            void ForceEditorPlayerLoopUpdate()            {                if (EditorApplication.isPlayingOrWillChangePlaymode || EditorApplication.isCompiling || EditorApplication.isUpdating)                {                    // Not in Edit mode, don't interfere                    return;                }                Update();            }        }#endif        //  将需要在主线程中执行的委托传递进来        public static void Post(Action task)        {            if (SynchronizationContext.Current == context)            {                task?.Invoke();            }            else            {                tasks.Enqueue(task);            }        }        static void Update()        {            if (tasks.TryDequeue(out var task))            {                task?.Invoke();            }        }    }}
本文的主角,Security-Camera-Toolkit-For-Unity 用到的 Loom 组件托管地址: 点我
用法:

/// <summary> /// NVR 登录 /// <para>执行登录逻辑之前通过<see cref="INVRStateHandler.OnLogin"/>向名下监控发送事件</para> /// </summary> public virtual async Task LoginAsync() {     foreach (var item in cameras)     {         Loom.Post(() => item.OnLogin(loginHandle));     }     await QueryCameraStatusAsync(true); }
在实际生产中的使用请 点我
结语:

    基于PlayerLoop  API 实现的 Loom 不依赖 Monobehaviour 组件,无需关注 Loom 生命周期。虽寥寥数行,却也实现了 Editor 下的 线程间通信,并且与播放时的使用不冲突。使用 Lambda 表达式的闭包优势,故而 Action 没有设计参数。使用 ConcurrentQueue 线程安全队列实现多线程共享任务列表,保证了线程安全。笔者仅仅在 PlayerLoopSystem 中的 Update 子系统中插入了自定义的方法 ,各位同学慎重把玩,笔者对用户自己行为造成的损失概不负责。
扩展阅读:

    UniTask.PlayerLoopHelper.cs - 思路参考,相当于是它的精简版本。SynchronizationContext  - .NET 框架中使用的同步线程间数据的类UnitySynchronizationContext - UnityEngine中的同步上下文组件,遥想刚发布时这个组件还存在死锁 bug ,所以呀,看待事物要以发展的眼光。PlayerLoop 由实验性的 API 转移到 Lowlevel 命名空间下,代表其趋于稳定,大家可以进一步了解,鉴于 Unity 版本众多,文档修正频繁,更多信息请移步官方 API Manual 并查询 PlayerLoop 关键字。
补充:

使用如下代码可以输出 Unity 中当前使用的所有的 PlayerLoopSyetem ,建议 LOG 单列显示


using UnityEngine;using UnityEngine.LowLevel;public static class Foo{    [RuntimeInitializeOnLoadMethod( RuntimeInitializeLoadType.BeforeSceneLoad)]    static void Install()    {        int ident = 0;        void ShowSystem(PlayerLoopSystem system)        {            ident++;            foreach (var item in system.subSystemList)            {                Debug.Log($"{new string('\t',ident)}{item .type}");                if (item.subSystemList?.Length>0)                {                    ShowSystem(item);                }            }            ident--;        }        var system = PlayerLoop.GetCurrentPlayerLoop();        ShowSystem(system);    }}

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2022-4-15 21:40 | 显示全部楼层

兄弟转载的话,能不能备注一下从哪儿转载的呀?
我是那个作者
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-22 13:44 , Processed in 0.068657 second(s), 24 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表