# 线程交叉访问助手
为确保脚本生命周期的执行顺序,辅助线程不能访问 Unity 的 API。但是,在使用多线程的过程中,有些线程会不可避免的调用 Unity 的 API,此时就需要制作一个助手类来解决这个问题。

# 代码部分
代码如下:
using System; | |
using System.Collections.Generic; | |
namespace Common | |
{ | |
/// <summary> | |
/// 线程交叉访问助手 | |
/// </summary> | |
public class ThreadCrossHelper : MonoSingleton<ThreadCrossHelper> | |
{ | |
/// <summary> | |
/// 行为信息类 | |
/// </summary> | |
private class ActionDelayItem | |
{ | |
/// <summary> | |
/// 行为 | |
/// </summary> | |
public Action Act; | |
/// <summary> | |
/// 延迟时间 | |
/// </summary> | |
public DateTime DelayTime; | |
/// <summary> | |
///true:可用 | |
/// </summary> | |
public bool Enabled; | |
/// <summary> | |
/// 创建行为信息 | |
/// </summary> | |
/// <param name="action"> 行为 & lt;/param> | |
/// <param name="time"> 延迟时间 & lt;/param> | |
public ActionDelayItem(Action action, DateTime time) | |
{ | |
Act = action; | |
DelayTime = time; | |
Enabled = true; | |
} | |
/// <summary> | |
/// 重置 | |
/// </summary> | |
/// <param name="action"> 行为 & lt;/param> | |
/// <param name="time"> 延迟时间 & lt;/param> | |
public void Reset(Action action, DateTime time) | |
{ | |
Act = action; | |
DelayTime = time; | |
Enabled = true; | |
} | |
} | |
/// <summary> | |
/// 行为列表 | |
/// </summary> | |
private List<ActionDelayItem> ActionList; | |
private void Start() | |
{ | |
ActionList = new List<ActionDelayItem>(); | |
} | |
private void Update() | |
{ | |
for (int i = ActionList.Count - 1; i >= 0; i--) | |
{ | |
if (ActionList[i].Enabled && DateTime.Now >= ActionList[i].DelayTime) | |
{ | |
ActionList[i].Act?.Invoke(); | |
ActionList[i].Enabled = false; | |
} | |
} | |
} | |
/// <summary> | |
/// 在主线程中调用指定的方法 | |
/// </summary> | |
/// <param name="act"> 委托 & lt;/param> | |
/// <param name="delay"> 延迟时间 & lt;/param> | |
public void ExecuteOnMainThread(Action act, float delay = 0) | |
{ | |
lock (ActionList) | |
{ | |
ActionDelayItem temp = ActionList.Find((iteam) => { return !iteam.Enabled; }); | |
if (temp != null) | |
{ | |
temp.Reset(act, DateTime.Now.AddSeconds(delay)); | |
} | |
else | |
{ | |
ActionList.Add(new ActionDelayItem(act, DateTime.Now.AddSeconds(delay))); | |
} | |
} | |
} | |
} | |
} |
# 使用方法
- 使用时必须先将脚本挂载到场景中的任意一个物体上
- 在其他线程中添加这段代码即可
ThreadCrossHelper.Instance.ExecuteOnMainThread(() => | |
{ | |
Debug.Log("Test"); | |
}, 0); |
# Unity 内置跨线程方法
# SynchronizationContext.Current
SynchronizationContext 是 .NET 提供的一个抽象类,表示代码应该通过什么机制进行调度和执行,SynchronizationContext.Current 则用于获取当前线程关联的同步上下文
Unity 会安装自己的 UnitySynchronizationContext,用于将任务调度到 Unity 主线程执行
在主线程中 Debug.Log (SynchronizationContext.Current?.GetType ().Name) 会得到 UnitySynchronizationContext
在其他线程上执行,通常会得到 null
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class ContextExample : MonoBehaviour
{
private SynchronizationContext mainContext;
void Awake()
{
// 必须在 Unity 主线程获取
mainContext = SynchronizationContext.Current;
}
void Start()
{
Task.Run(() =>
{
// 当前运行在后台线程
int result = 100 + 200;
// 第一个参数:需要调度执行的回调函数,实际上是一个委托
// 第二个参数:传递给回调的数据,可以是任意对象。如果不需要传递数据,直接使用 null
mainContext.Post(_ =>
{
// 这里在 Unity 主线程执行
transform.position =
new Vector3(result, 0, 0);
Debug.Log("已经返回主线程");
}, null);
});
}
}
如果异步方法从 Unity 主线程开始执行,await Task 会自动捕获当前的 SynchronizationContext,并在等待结束后返回主线程,而 ConfigureAwait (false) 会改变这一行为
注意 Post () 和 Send () 的区别,正常情况下优先使用 Post () 或 await
# Awaitable
Awaitable 只能在 2023 及以上的版本中使用
这种写法比手动使用 SynchronizationContext.Post () 更直观
private async Awaitable SaveAsync()
{
// 切换到后台线程
await Awaitable.BackgroundThreadAsync();
// 进行纯托管数据的序列化等操作
byte[] bytes = SerializeData();
// 返回 Unity 主线程
await Awaitable.MainThreadAsync();
// 访问 Unity 对象
Debug.Log($"保存完成:{bytes.Length}");
}