|
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); }} |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|