# 线程交叉访问助手

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

img

# 代码部分

  • 助手类只需要一个,所以使用单例模式
  • 使用委托调用需要执行的代码
  • 提供延时调用的功能

代码如下:

ThreadCrossHelper
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}");
}