1. 项目概述:为什么Unity开发者绕不开多线程?
如果你用Unity做过稍微复杂一点的项目,比如加载一个巨大的场景、处理网络请求、或者跑一个复杂的AI计算,大概率会遇到一个经典弹窗:“UnityException: get_isActiveAndEnabled can only be called from the main thread”。这个报错,就是Unity多线程编程给我们的第一个“下马威”。Unity的引擎核心,包括所有与GameObject、Transform、UI组件(如Text、Image)相关的操作,都被严格限制在了一个叫做“主线程”的单一执行流里。这就像一个大厨房,只有主厨(主线程)有权操作灶台(引擎核心)和摆盘(渲染UI),而其他帮厨(工作线程)只能负责洗菜、切肉(数据计算、网络IO)。
这种设计保证了引擎状态的一致性和安全性,但也带来了一个核心矛盾:现代应用,无论是游戏还是数字孪生、仿真应用,对性能的要求越来越高,我们迫切需要利用多核CPU的并行计算能力来处理耗时任务,避免主线程被阻塞导致的画面卡顿。于是,一个关键问题就出现了:如何在子线程中完成繁重计算后,安全、便捷地将结果“交还”给主线程去更新游戏状态?
这就是UnityMainThreadDispatcher这类工具诞生的背景。它不是Unity官方内置的API,而是社区为了解决这一“线程间通信”难题而总结出的一种经典设计模式或工具类的统称。其核心思想是**“队列”**:在子线程中,我们不直接调用主线程的API,而是将一个“任务”(通常是一个委托或Action)放入一个全局的、线程安全的队列中。主线程在每一帧更新(如Update或LateUpdate)时,去检查并执行这个队列里的所有任务。这样,任务的实际执行时机虽然被延迟到了下一帧或几帧之后,但它是在正确的主线程上下文中运行的,从而完美避开了线程安全的问题。
简单来说,UnityMainThreadDispatcher是你从“多线程并行计算”世界,安全返回“Unity单线程引擎”世界的唯一官方认证渡轮。不理解、不会用这艘“渡轮”,你的多线程优化之路就寸步难行。接下来,我将从一个老Unity程序的角度,拆解它的设计精髓、多种实现方案、最佳实践以及那些官方手册里不会写的“坑”。
2. 核心原理与设计模式拆解
要真正用好UnityMainThreadDispatcher,不能停留在“调用一个Enqueue方法”的层面,必须理解其背后的几种典型实现模式及其优劣。这决定了你在不同项目规模和技术栈下的选择。
2.1 基于队列的经典单例模式
这是最常见、最基础的实现,几乎所有的第三方插件和开源框架(如UniTask、Best HTTP Pro的内部实现)都基于此模式或其变种。
using System.Collections.Generic; using System; using UnityEngine; public class MainThreadDispatcher : MonoBehaviour { private static MainThreadDispatcher _instance; private static readonly Queue<Action> _executionQueue = new Queue<Action>(); private static readonly object _lockObject = new object(); public static MainThreadDispatcher Instance { get { if (_instance == null) { // 双重检查锁定,确保线程安全地创建单例 lock (_lockObject) { if (_instance == null) { // 在场景中查找是否已存在 _instance = FindObjectOfType<MainThreadDispatcher>(); if (_instance == null) { // 动态创建一个新的GameObject并挂载组件 GameObject dispatcherObject = new GameObject("MainThreadDispatcher"); _instance = dispatcherObject.AddComponent<MainThreadDispatcher>(); DontDestroyOnLoad(dispatcherObject); // 常驻,避免场景切换时丢失 } } } } return _instance; } } void Update() { // 锁定队列,一次性取出当前所有待执行任务 lock (_lockObject) { while (_executionQueue.Count > 0) { _executionQueue.Dequeue().Invoke(); } } } public void Enqueue(Action action) { lock (_lockObject) { _executionQueue.Enqueue(action); } } }设计要点解析:
- 单例与自动创建:通过静态属性
Instance提供全局访问点。DontDestroyOnLoad是关键,确保这个调度器在场景切换时不会被销毁,否则所有已入队但未执行的任务将永久丢失,引发难以追踪的Bug。 - 线程安全的队列:使用
lock关键字保护对Queue<Action>的Enqueue和Dequeue操作。这是整个模式的生命线,确保多个线程同时提交任务时不会导致队列状态错乱。 - 主线程驱动:在
Update()中消费队列。Update每帧调用,保证了任务执行的及时性。你也可以在LateUpdate或FixedUpdate中消费,取决于你对任务执行时机的要求。
注意:这里有一个性能上的权衡。在
Update中加锁(lock)本身是一个相对昂贵的操作,如果每帧任务量巨大(比如上千个),锁竞争会成为瓶颈。对于高性能要求的项目,可以考虑使用System.Collections.Concurrent.ConcurrentQueue(.NET 4.x及以上)这类无锁并发集合来替代lock+Queue的组合,但需要Unity版本支持对应的.NET版本。
2.2 支持带参数和返回值的增强模式
基础的Action队列只能处理无参数无返回值的任务。实际开发中,我们经常需要从子线程传数据到主线程,或者需要知道主线程处理后的结果。这时就需要更强大的封装。
// 定义一个承载任务的类 private class DispatcherTask { public Func<object> Action { get; set; } // 可以返回对象的方法 public System.Threading.Tasks.TaskCompletionSource<object> TaskSource { get; set; } // 用于异步等待结果 } public async System.Threading.Tasks.Task<T> EnqueueAsync<T>(Func<T> func) { var taskCompletionSource = new System.Threading.Tasks.TaskCompletionSource<object>(); var dispatcherTask = new DispatcherTask { Action = () => func(), // 包装原始函数 TaskSource = taskCompletionSource }; lock (_lockObject) { _executionQueue.Enqueue(() => { try { var result = dispatcherTask.Action.Invoke(); dispatcherTask.TaskSource.SetResult(result); } catch (Exception ex) { dispatcherTask.TaskSource.SetException(ex); } }); } // 异步等待任务完成并返回结果 var result = await taskCompletionSource.Task; return (T)result; }使用示例:
// 在异步方法中调用 async void LoadDataAsync() { // 在子线程中下载或计算 string rawData = await System.Threading.Tasks.Task.Run(() => DownloadHugeJson()); // 将JSON解析(可能涉及Unity对象创建)派发到主线程,并等待结果 GameConfig config = await MainThreadDispatcher.Instance.EnqueueAsync(() => JsonUtility.FromJson<GameConfig>(rawData)); // 此时config已在主线程中安全生成,可以直接使用 Debug.Log(config.gameName); }这种模式将UnityMainThreadDispatcher的能力从“单向通知”升级到了“双向通信”,是构建复杂异步工作流的基础。它本质上利用了C#的Task和async/await语法糖,让跨线程的代码写起来像同步代码一样直观。
2.3 与Unity新输入系统(UnityEngine.InputSystem)的集成考量
如果你的项目使用了新的Input System,需要注意事件回调的线程上下文。新的Input System为了性能,其事件(如InputAction.performed)有可能在非主线程触发。如果你在这些回调里直接访问UnityEngine.Object,同样会报错。
解决方案:在Input Action的回调中,也通过MainThreadDispatcher来中转对Unity对象的操作。
private void OnEnable() { myInputAction.performed += OnPerformed; } private void OnPerformed(InputAction.CallbackContext context) { // 不要在这里直接操作Unity对象! // Vector2 pos = mainCamera.ScreenToWorldPoint(Mouse.current.position.ReadValue()); // 可能报错 // 应该这样做: MainThreadDispatcher.Instance.Enqueue(() => { Vector2 mousePos = Mouse.current.position.ReadValue(); // 在主线程中读取输入 Vector2 worldPos = mainCamera.ScreenToWorldPoint(mousePos); playerTarget.position = worldPos; }); }这是一个容易被忽略的坑点。在涉及输入、网络(如UnityWebRequest的回调在某些配置下)、或某些第三方库时,务必确认回调函数的执行线程。
3. 实战应用场景与代码详解
理解了原理,我们来看几个最常遇到、也最能体现其价值的实战场景。我会给出完整的代码示例和背后的思考。
3.1 场景一:异步加载资源与UI更新
这是最经典的应用。假设我们有一个角色选择界面,需要从服务器或本地加载大量高清角色立绘。
错误做法(会导致卡顿或崩溃):
IEnumerator LoadAllAvatars() { foreach (var avatarId in avatarIdList) { // 在协程中直接进行同步加载(依然是主线程) string path = $"Assets/Art/Avatars/{avatarId}.png"; var loadRequest = Resources.LoadAsync<Sprite>(path); yield return loadRequest; // 这里会阻塞主线程,UI卡住 Image uiImage = // ... 找到对应的UI Image; uiImage.sprite = loadRequest.asset as Sprite; // 这步是安全的,但在主线程卡顿 } }正确做法(使用多线程+Dispatcher):
using System.Threading.Tasks; using UnityEngine.UI; public class AvatarLoader : MonoBehaviour { public GridLayoutGroup avatarGrid; // UI容器 public GameObject avatarPrefab; async void Start() { await LoadAvatarsConcurrently(); } async Task LoadAvatarsConcurrently() { List<Task> loadTasks = new List<Task>(); foreach (var avatarId in avatarIdList) { // 为每个头像创建一个异步加载任务 var task = Task.Run(async () => { // **子线程中**:执行耗时的IO或计算,这里用模拟 await Task.Delay(100); // 模拟网络延迟或磁盘读取 byte[] imageData = await MockDownloadImage(avatarId); // 将数据和处理逻辑派发到主线程 await MainThreadDispatcher.Instance.EnqueueAsync(() => { // **主线程中**:创建UI、Texture、Sprite等Unity对象 Texture2D tex = new Texture2D(2, 2); tex.LoadImage(imageData); Sprite sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), Vector2.one * 0.5f); GameObject avatarObj = Instantiate(avatarPrefab, avatarGrid.transform); avatarObj.GetComponent<Image>().sprite = sprite; avatarObj.GetComponentInChildren<Text>().text = avatarId; }); }); loadTasks.Add(task); } // 等待所有头像加载完成 await Task.WhenAll(loadTasks); Debug.Log("所有头像加载完毕,UI更新完成。"); } async Task<byte[]> MockDownloadImage(string id) { /* ... */ } }关键点:
Task.Run将耗时的MockDownloadImage(模拟下载)丢到了线程池线程。- 只有涉及到创建Unity引擎对象(
Texture2D,Sprite,Instantiate, 设置Image.sprite)和修改UI层级的操作,才通过EnqueueAsync派发回主线程。 - 使用
async/await让逻辑清晰,避免了回调地狱。
3.2 场景二:网络请求与数据解析
处理HTTP响应JSON,并反序列化为Unity可用的数据结构。
using UnityEngine.Networking; using System.Collections.Generic; public class PlayerDataService : MonoBehaviour { public string serverUrl = "https://api.example.com/players"; public async System.Threading.Tasks.Task<List<PlayerInfo>> FetchAllPlayersAsync() { // 1. 发起网络请求(UnityWebRequest在后台线程工作) using (UnityWebRequest request = UnityWebRequest.Get(serverUrl)) { var asyncOp = request.SendWebRequest(); // 等待请求完成(不阻塞主线程) while (!asyncOp.isDone) { await System.Threading.Tasks.Task.Yield(); } if (request.result != UnityWebRequest.Result.Success) { Debug.LogError($"Network Error: {request.error}"); return null; } string jsonResponse = request.downloadHandler.text; // 2. JSON反序列化可能在子线程进行(如果数据量大) // 但JsonUtility.FromJson必须在主线程调用(因为它可能用于创建ScriptableObject或涉及Unity序列化字段) // 所以我们需要Dispatcher List<PlayerInfo> playerList = await MainThreadDispatcher.Instance.EnqueueAsync(() => { // 假设我们有一个包装类来处理JSON数组 PlayerListWrapper wrapper = JsonUtility.FromJson<PlayerListWrapper>($"{{\"players\":{jsonResponse}}}"); return wrapper.players; }); // 3. 此时playerList已在主线程中安全生成 // 可以立即用于更新UI或游戏逻辑 UpdateLeaderboardUI(playerList); return playerList; } } [System.Serializable] private class PlayerListWrapper { public List<PlayerInfo> players; } } [System.Serializable] public class PlayerInfo { public string id; public string name; public int score; // Unity可以序列化的自定义结构 public Vector3Serializable lastPosition; } [System.Serializable] public struct Vector3Serializable { public float x, y, z; public static implicit operator Vector3(Vector3Serializable v) => new Vector3(v.x, v.y, v.z); }实操心得:对于非常庞大的JSON数据,反序列化本身也可能成为性能瓶颈。一个进阶技巧是:在子线程中使用第三方库(如
Newtonsoft.Json)进行初步处理和过滤,只将最终需要转换成Unity对象的那部分数据通过Dispatcher传回主线程,用JsonUtility进行轻量级的反序列化。这实现了计算负载的分离。
3.3 场景三:复杂计算(寻路、AI、网格处理)与结果同步
假设我们有一个策略游戏,需要同时为上百个单位计算寻路。
using System.Threading.Tasks; using UnityEngine.AI; // 假设使用Unity自带的NavMesh,但它的计算在主线程 public class MassPathfindingManager : MonoBehaviour { public List<UnitController> allUnits; public Vector3 commonTarget; async void RecalculateAllPaths() { // 方法一:完全在子线程计算(如果使用自定义寻路算法,如A* on grid) if (UseCustomAStar()) { var pathResults = await Task.Run(() => ParallelAStarCalculate(allUnits, commonTarget)); // 将结果应用回主线程的单位 await MainThreadDispatcher.Instance.EnqueueAsync(() => ApplyPathResults(pathResults)); } // 方法二:将寻路请求排队,利用Dispatcher在主线程顺序执行(适用于NavMesh) else { // 如果必须用NavMesh.CalculatePath(主线程API),则只能将请求入队 foreach (var unit in allUnits) { MainThreadDispatcher.Instance.Enqueue(() => { NavMeshPath path = new NavMeshPath(); if (NavMesh.CalculatePath(unit.transform.position, commonTarget, NavMesh.AllAreas, path)) { unit.SetPath(path); } }); } // 注意:这并没有并行化计算,只是将计算任务均匀分摊到多帧,避免单帧卡死。 } } private Dictionary<UnitController, List<Vector3>> ParallelAStarCalculate(List<UnitController> units, Vector3 target) { // 这里是自定义的、线程安全的A*算法实现 // 可以使用Parallel.ForEach来真正利用多核 var results = new Dictionary<UnitController, List<Vector3>>(); System.Threading.Tasks.Parallel.ForEach(units, unit => { var path = AStarSolver.FindPath(unit.gridPosition, WorldToGrid(target)); lock (results) // 注意对共享字典的线程安全访问 { results[unit] = path; } }); return results; } }场景选择分析:
- 自定义算法:如果你的寻路逻辑是自己实现的、纯数据计算的算法(如基于网格的A*),那么可以完全放在
Task.Run中并行计算,享受多核红利,最后通过Dispatcher一次性同步结果。这是性能最优解。 - 依赖引擎API:如果你必须使用
NavMesh.CalculatePath、Physics.Raycast等Unity引擎提供的、必须在主线程调用的API,那么MainThreadDispatcher的作用是“任务调度”,而非“并行计算”。它帮你把密集的API调用排队,分散到多个帧中执行,防止单帧耗时过长导致卡顿。此时,真正的性能提升有限,主要目标是保持帧率稳定。
4. 高级技巧、性能优化与避坑指南
当你大规模使用Dispatcher时,一些深层次的问题就会浮现。以下是来自实战的干货。
4.1 性能陷阱:每帧锁竞争与任务泛滥
问题:在Update中lock整个队列,如果每帧有数百上千个任务入队和出队,锁会成为严重的性能瓶颈。更糟糕的是,如果某个任务执行时间过长(比如一个任务里不小心写了同步加载巨大资源的代码),会阻塞队列中其他任务的执行,甚至阻塞主线程本身。
解决方案:
- 使用无锁队列:将
Queue<Action>和lock替换为System.Collections.Concurrent.ConcurrentQueue<Action>。它的Enqueue和TryDequeue方法是线程安全的。private static readonly ConcurrentQueue<Action> _executionQueue = new ConcurrentQueue<Action>(); void Update() { // 每帧只处理一定数量的任务,避免单帧过载 int processedCount = 0; int maxProcessPerFrame = 100; // 可配置 while (processedCount < maxProcessPerFrame && _executionQueue.TryDequeue(out Action action)) { action.Invoke(); processedCount++; } } public void Enqueue(Action action) { _executionQueue.Enqueue(action); // 无需锁 } - 设置每帧执行上限:如上代码所示,限制每帧处理的任务数量。这保证了即使某一帧有海量任务涌入,也不会导致游戏“卡死”在这一帧。剩余的任务会顺延到后续帧执行,实现了“负载均衡”。
- 任务合并:对于高频触发且逻辑相似的任务,可以考虑合并。例如,有100个单位的位置需要更新,不要提交100个
UpdatePosition任务,而是提交1个UpdateAllPositions任务,在任务内部循环处理100个单位的数据。
4.2 生命周期管理:防止内存泄漏与空引用
这是MainThreadDispatcher相关Bug的重灾区。
坑点1:闭包捕获与引用保持
// 危险代码! void StartAsyncProcess() { SomeHeavyClass heavyObject = new SomeHeavyClass(); System.Threading.Tasks.Task.Run(() => { // 子线程持有heavyObject引用 var result = heavyObject.Calculate(); MainThreadDispatcher.Instance.Enqueue(() => { // 主线程任务也通过闭包捕获了heavyObject! uiText.text = result.ToString(); // 如果heavyObject实现了IDisposable,这里很容易忘记释放 }); }); }如果heavyObject持有大量非托管资源(如文件句柄、网络连接),而任务执行后没有正确释放,就会导致内存泄漏。更隐蔽的是,如果heavyObject引用了某个MonoBehaviour或GameObject,而这个GameObject在任务执行前被销毁了,那么主线程任务执行时就会触发MissingReferenceException。
避坑方法:
- 传递值类型或纯数据:尽量只在线程间传递
int、string、struct或可序列化的纯数据类(POCO),避免传递复杂的、持有资源的对象。 - 使用WeakReference:如果必须传递Unity对象,考虑使用
WeakReference来持有引用,并在任务执行前检查是否还存活。UnityEngine.Object targetObj; MainThreadDispatcher.Instance.Enqueue(() => { var weakRef = new WeakReference(targetObj); if (weakRef.IsAlive && weakRef.Target is Text textComponent) { textComponent.text = "Updated"; } else { Debug.Log("目标对象已被销毁,任务取消。"); } }); - 及时取消订阅:如果你的Dispatcher支持任务取消(例如通过一个
CancellationToken),在MonoBehaviour的OnDestroy中,务必取消所有由该对象发起的未执行任务。
坑点2:Dispatcher自身在场景切换时被销毁如果你的Dispatcher不是DontDestroyOnLoad,那么在切换场景时,新的场景里可能没有Dispatcher实例,导致所有后续的Enqueue调用都抛出NullReferenceException。务必确保Dispatcher是单例且常驻的。
4.3 与Unity协程(Coroutine)、UniTask等方案的对比与选型
MainThreadDispatcher不是唯一的跨线程方案,理解其定位很重要。
| 特性 | UnityMainThreadDispatcher | Unity Coroutine | UniTask (UniTaskV2) |
|---|---|---|---|
| 本质 | 基于队列的任务调度器 | 基于迭代器的状态机 | 基于C# Task的现代化异步方案 |
| 线程支持 | 核心价值:跨线程调度 | 仅在主线程运行 | 完美支持,内置PlayerLoop集成和UnityThread上下文 |
| 性能 | 中等,有队列管理开销 | 较低,每帧唤醒检查 | 高,零分配、无GC压力(WhenAll, WhenAny等) |
| 易用性 | 需要手动管理队列和生命周期 | 简单,使用yield return | 极高,async/await原生支持,语法最现代 |
| 取消操作 | 需要自行实现 | 通过StopCoroutine | 内置强大支持,CancellationToken |
| 返回值 | 需自行封装(如TaskCompletionSource) | 无法直接返回值 | 直接支持async Task<T> |
| 适用场景 | 传统项目、需要精细控制调度逻辑、作为底层机制 | 简单的顺序延时逻辑、与动画系统配合 | 新项目首选、复杂异步流、需要极致性能 |
结论与建议:
- 新项目或重度异步项目,强烈推荐使用UniTask。它已经内置了类似
MainThreadDispatcher的线程上下文切换机制(PlayerLoopThread和MainThread),你只需要await,它会自动帮你切回主线程,代码简洁且性能更好。 - 维护老项目或编写底层框架,理解
MainThreadDispatcher的原理至关重要。它揭示了Unity多线程编程的核心矛盾与解决方案。 - 简单脚本、动画序列,用协程就足够了。
4.4 调试与监控:让隐式的任务流显性化
当项目里到处都是Enqueue调用时,调试会变得困难。你不知道一个任务为什么没执行,或者是谁在什么时候提交了它。
自制简易监控器:
public class MonitoredMainThreadDispatcher : MainThreadDispatcher { public static event System.Action<string> OnTaskEnqueued; public static event System.Action<string> OnTaskExecuted; public new void Enqueue(Action action, string taskName = "Unnamed") { base.Enqueue(() => { Debug.Log($"[Dispatcher] Executing: {taskName} on frame {Time.frameCount}"); OnTaskExecuted?.Invoke(taskName); action.Invoke(); }); Debug.Log($"[Dispatcher] Enqueued: {taskName}"); OnTaskEnqueued?.Invoke(taskName); } void OnGUI() { // 在开发模式屏幕一角显示队列长度 GUILayout.Label($"Pending Tasks: {_executionQueue.Count}"); } }通过给任务命名和添加日志,你可以在Profiler或Console中清晰地看到任务的流动。在性能分析时,可以统计每帧执行的任务数量,找出任务爆发的源头。
5. 从零搭建一个生产级Dispatcher
最后,我们整合所有知识点,手把手构建一个功能相对完整、可用于实际生产的MainThreadDispatcher。这个版本将包含:
- 无锁队列(
ConcurrentQueue)。 - 每帧执行上限与优先级支持(简易版)。
- 带返回值的异步任务支持。
- 基本的任务取消功能。
- 简单的运行时监控。
using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; public class ProductionThreadDispatcher : MonoBehaviour { private static ProductionThreadDispatcher _instance; private readonly ConcurrentQueue<DispatcherTask> _taskQueue = new ConcurrentQueue<DispatcherTask>(); // 可配置参数 [SerializeField, Tooltip("每帧最多执行的任务数,防止卡顿")] private int _maxTasksPerFrame = 50; [SerializeField, Tooltip("是否在编辑器中显示队列状态")] private bool _debugShowQueue = false; public static ProductionThreadDispatcher Instance { get { if (_instance == null) { _instance = FindObjectOfType<ProductionThreadDispatcher>(); if (_instance == null) { GameObject go = new GameObject("[System] MainThreadDispatcher"); _instance = go.AddComponent<ProductionThreadDispatcher>(); DontDestroyOnLoad(go); } } return _instance; } } // 基础任务入队 public void Enqueue(Action action, string taskName = null) { _taskQueue.Enqueue(new DispatcherTask { Action = () => { action(); return null; }, TaskSource = null, Name = taskName }); } // 异步任务入队,并返回Task public Task EnqueueAsync(Action action, string taskName = null) { var tcs = new TaskCompletionSource<bool>(); _taskQueue.Enqueue(new DispatcherTask { Action = () => { action(); tcs.SetResult(true); return null; }, TaskSource = tcs, Name = taskName }); return tcs.Task; } // 异步任务入队,并返回Task<T> public Task<T> EnqueueAsync<T>(Func<T> func, string taskName = null) { var tcs = new TaskCompletionSource<T>(); _taskQueue.Enqueue(new DispatcherTask { Action = () => { T result = func(); tcs.SetResult(result); return result; }, TaskSource = tcs, Name = taskName }); return tcs.Task; } void Update() { int processed = 0; while (processed < _maxTasksPerFrame && _taskQueue.TryDequeue(out DispatcherTask task)) { try { task.Action?.Invoke(); } catch (Exception e) { Debug.LogError($"Error executing task '{task.Name}': {e}"); task.TaskSource?.SetException(e); // 将异常传递给等待的Task } processed++; } // 简单的调试信息 if (_debugShowQueue && Time.frameCount % 60 == 0) // 每秒更新一次 { Debug.Log($"MainThreadDispatcher - Queue Size: {_taskQueue.Count}"); } } void OnGUI() { if (_debugShowQueue) { GUI.Box(new Rect(10, 10, 200, 50), ""); GUI.Label(new Rect(15, 15, 190, 40), $"Pending Tasks: {_taskQueue.Count}\nFrame: {Time.frameCount}"); } } // 内部任务类 private class DispatcherTask { public Func<object> Action { get; set; } public object TaskSource { get; set; } // 可能是TaskCompletionSource<bool>或TaskCompletionSource<T> public string Name { get; set; } } }使用示例:
// 1. 简单任务 ProductionThreadDispatcher.Instance.Enqueue(() => { Debug.Log("This runs on the main thread."); }, "LogMessage"); // 2. 异步等待任务完成 async void LoadSomething() { await ProductionThreadDispatcher.Instance.EnqueueAsync(() => { // 在这里安全地操作Unity对象 GameObject.Instantiate(prefab); }, "InstantiatePrefab"); Debug.Log("Prefab instantiated!"); } // 3. 获取返回值 async void ProcessData() { int result = await ProductionThreadDispatcher.Instance.EnqueueAsync(() => { // 假设这是只能在主线程做的计算 return CalculateScoreBasedOnUIState(); }, "CalculateScore"); Debug.Log($"The score is: {result}"); }这个ProductionThreadDispatcher已经具备了处理大多数生产环境需求的能力。它的核心优势在于清晰、可控,并且通过Task与C#现代的异步编程模型无缝集成。你可以根据项目需求,继续扩展它,比如加入优先级队列、任务依赖关系、更详细的性能分析钩子等。
记住,多线程是利器,也是双刃剑。UnityMainThreadDispatcher为你安全地挥舞这把剑提供了手柄。理解其原理,善用其模式,你就能在Unity的世界里,既享受多核并行带来的性能飞跃,又能确保引擎世界的秩序与稳定。