Unity中使用多线程

Stackoverflow原文链接

正文

       Unity是基于框架开发的,因此Unity是线程不安全的。Unity里有一个机制,当其他进程访问Unity API时,会抛出一个异常,这就导致其他进程无法直接访问Unity API。举例来说,要控制一个物体的移动旋转,只能在主线程中操作,子线程没有办法使用GetComponent这样的API,但是子线程可以计算位移、旋转角度(即子线程可以使用Unity中的基本数据结构int、Vector3、Quaternion等)。

       在Stackoverflow或Unity论坛网站上,通常会看到的解决方案是使用一个布尔变量来让主线线程知道你需要在主线线程中执行代码。这样做是不对的,因为它不是线程安全的,并且不能控制调用哪个函数。如果有多个线程需要通知主线程怎么办?

       另一种解决方案是使用协同程序(StartCoroutine)而不是线程。这是行不通的。对Socket来说,使用协程不会改变任何东西。还是要使用线程代码或使用异步。

       一个正确的方法时创建一个集合(比如List),当你需要在主线程中执行某些操作时,调用那些存储在Action(委托)中的方法。将委托列表复制到本地,然后从本地委托列表执行方法,执行后从列表中清除,这样可以防止其他线程一直在等待,造成阻塞。

       还需要添加一个bool变量,以通知Update函数,在列表中有代码等待执行。当将该列表复制到一个本地列表时,应该将其封装在lock关键字周围,以防止其他线程写入该列表,保证线程安全。

UnityThread 脚本:

#define ENABLE_UPDATE_FUNCTION_CALLBACK
#define ENABLE_LATEUPDATE_FUNCTION_CALLBACK
#define ENABLE_FIXEDUPDATE_FUNCTION_CALLBACK

using System;
using System.Collections;
using UnityEngine;
using System.Collections.Generic;


public class UnityThread : MonoBehaviour
{
    //our (singleton) instance
    private static UnityThread instance = null;


    UPDATE IMPL
    //Holds actions received from another Thread. Will be coped to actionCopiedQueueUpdateFunc then executed from there
    private static List<System.Action> actionQueuesUpdateFunc = new List<Action>();

    //holds Actions copied from actionQueuesUpdateFunc to be executed
    List<System.Action> actionCopiedQueueUpdateFunc = new List<System.Action>();

    // Used to know if whe have new Action function to execute. This prevents the use of the lock keyword every frame
    private volatile static bool noActionQueueToExecuteUpdateFunc = true;


    LATEUPDATE IMPL
    //Holds actions received from another Thread. Will be coped to actionCopiedQueueLateUpdateFunc then executed from there
    private static List<System.Action> actionQueuesLateUpdateFunc = new List<Action>();

    //holds Actions copied from actionQueuesLateUpdateFunc to be executed
    List<System.Action> actionCopiedQueueLateUpdateFunc = new List<System.Action>();

    // Used to know if whe have new Action function to execute. This prevents the use of the lock keyword every frame
    private volatile static bool noActionQueueToExecuteLateUpdateFunc = true;



    FIXEDUPDATE IMPL
    //Holds actions received from another Thread. Will be coped to actionCopiedQueueFixedUpdateFunc then executed from there
    private static List<System.Action> actionQueuesFixedUpdateFunc = new List<Action>();

    //holds Actions copied from actionQueuesFixedUpdateFunc to be executed
    List<System.Action> actionCopiedQueueFixedUpdateFunc = new List<System.Action>();

    // Used to know if whe have new Action function to execute. This prevents the use of the lock keyword every frame
    private volatile static bool noActionQueueToExecuteFixedUpdateFunc = true;


    //Used to initialize UnityThread. Call once before any function here
    public static void initUnityThread(bool visible = false)
    {
        if (instance != null)
        {
            return;
        }

        if (Application.isPlaying)
        {
            // add an invisible game object to the scene
            GameObject obj = new GameObject("MainThreadExecuter");
            if (!visible)
            {
                obj.hideFlags = HideFlags.HideAndDontSave;
            }

            DontDestroyOnLoad(obj);
            instance = obj.AddComponent<UnityThread>();
        }
    }

    public void Awake()
    {
        DontDestroyOnLoad(gameObject);
    }

    //COROUTINE IMPL//
#if (ENABLE_UPDATE_FUNCTION_CALLBACK)
    public static void executeCoroutine(IEnumerator action)
    {
        if (instance != null)
        {
            executeInUpdate(() => instance.StartCoroutine(action));
        }
    }

    UPDATE IMPL
    public static void executeInUpdate(System.Action action)
    {
        if (action == null)
        {
            throw new ArgumentNullException("action");
        }

        lock (actionQueuesUpdateFunc)
        {
            actionQueuesUpdateFunc.Add(action);
            noActionQueueToExecuteUpdateFunc = false;
        }
    }

    public void Update()
    {
        if (noActionQueueToExecuteUpdateFunc)
        {
            return;
        }

        //Clear the old actions from the actionCopiedQueueUpdateFunc queue
        actionCopiedQueueUpdateFunc.Clear();
        lock (actionQueuesUpdateFunc)
        {
            //Copy actionQueuesUpdateFunc to the actionCopiedQueueUpdateFunc variable
            actionCopiedQueueUpdateFunc.AddRange(actionQueuesUpdateFunc);
            //Now clear the actionQueuesUpdateFunc since we've done copying it
            actionQueuesUpdateFunc.Clear();
            noActionQueueToExecuteUpdateFunc = true;
        }

        // Loop and execute the functions from the actionCopiedQueueUpdateFunc
        for (int i = 0; i < actionCopiedQueueUpdateFunc.Count; i++)
        {
            actionCopiedQueueUpdateFunc[i].Invoke();
        }
    }
#endif

    LATEUPDATE IMPL
#if (ENABLE_LATEUPDATE_FUNCTION_CALLBACK)
    public static void executeInLateUpdate(System.Action action)
    {
        if (action == null)
        {
            throw new ArgumentNullException("action");
        }

        lock (actionQueuesLateUpdateFunc)
        {
            actionQueuesLateUpdateFunc.Add(action);
            noActionQueueToExecuteLateUpdateFunc = false;
        }
    }


    public void LateUpdate()
    {
        if (noActionQueueToExecuteLateUpdateFunc)
        {
            return;
        }

        //Clear the old actions from the actionCopiedQueueLateUpdateFunc queue
        actionCopiedQueueLateUpdateFunc.Clear();
        lock (actionQueuesLateUpdateFunc)
        {
            //Copy actionQueuesLateUpdateFunc to the actionCopiedQueueLateUpdateFunc variable
            actionCopiedQueueLateUpdateFunc.AddRange(actionQueuesLateUpdateFunc);
            //Now clear the actionQueuesLateUpdateFunc since we've done copying it
            actionQueuesLateUpdateFunc.Clear();
            noActionQueueToExecuteLateUpdateFunc = true;
        }

        // Loop and execute the functions from the actionCopiedQueueLateUpdateFunc
        for (int i = 0; i < actionCopiedQueueLateUpdateFunc.Count; i++)
        {
            actionCopiedQueueLateUpdateFunc[i].Invoke();
        }
    }
#endif

    FIXEDUPDATE IMPL//
#if (ENABLE_FIXEDUPDATE_FUNCTION_CALLBACK)
    public static void executeInFixedUpdate(System.Action action)
    {
        if (action == null)
        {
            throw new ArgumentNullException("action");
        }

        lock (actionQueuesFixedUpdateFunc)
        {
            actionQueuesFixedUpdateFunc.Add(action);
            noActionQueueToExecuteFixedUpdateFunc = false;
        }
    }

    public void FixedUpdate()
    {
        if (noActionQueueToExecuteFixedUpdateFunc)
        {
            return;
        }

        //Clear the old actions from the actionCopiedQueueFixedUpdateFunc queue
        actionCopiedQueueFixedUpdateFunc.Clear();
        lock (actionQueuesFixedUpdateFunc)
        {
            //Copy actionQueuesFixedUpdateFunc to the actionCopiedQueueFixedUpdateFunc variable
            actionCopiedQueueFixedUpdateFunc.AddRange(actionQueuesFixedUpdateFunc);
            //Now clear the actionQueuesFixedUpdateFunc since we've done copying it
            actionQueuesFixedUpdateFunc.Clear();
            noActionQueueToExecuteFixedUpdateFunc = true;
        }

        // Loop and execute the functions from the actionCopiedQueueFixedUpdateFunc
        for (int i = 0; i < actionCopiedQueueFixedUpdateFunc.Count; i++)
        {
            actionCopiedQueueFixedUpdateFunc[i].Invoke();
        }
    }
#endif

    public void OnDisable()
    {
        if (instance == this)
        {
            instance = null;
        }
    }
}

用法:

      这个脚本允许我们调用3个最常用的Unity函数:Update, LateUpdate和FixedUpdate函数。还允许我们在主线程中调用运行协同程序函数。它甚至可以被扩展到能够调用其他Unity回调函数如OnPreRender和OnPostRender。

1.首先,从Awake()函数初始化它

void Awake()
{
    UnityThread.initUnityThread();
}

2.从另一个线程执行主线程中的代码:

UnityThread.executeInUpdate(() =>
{
    transform.Rotate(new Vector3(0f, 90f, 0f));
});

这会将该脚本的当前对象旋转90°

3.从另一个线程调用主线程中的函数:

Action rot = Rotate;
UnityThread.executeInUpdate(rot);

void Rotate()
{
    transform.Rotate(new Vector3(0f, 90f, 0f));
}

#2和#3示例在更新函数中执行。

4.从另一个线程执行LateUpdate函数中的代码:

UnityThread.executeInLateUpdate(()=>
{
    //Your code
});

5.从另一个线程执行FixedUpdate函数中的代码:

UnityThread.executeInFixedUpdate(()=>
{
    //Your code
});

6.从另一个线程在主线程中启动一个协同函数:

UnityThread.executeCoroutine(myCoroutine());

IEnumerator myCoroutine()
{
    Debug.Log("Hello");
    yield return new WaitForSeconds(2f);
    Debug.Log("Test");
}

最后,如果你不需要执行LateUpdate和FixedUpdate函数中的任何内容,注释下面这两行代码即可:

//#define ENABLE_LATEUPDATE_FUNCTION_CALLBACK
//#define ENABLE_FIXEDUPDATE_FUNCTION_CALLBACK

这将提高性能。

  • 5
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Unity使用多线程可以提高游戏的性能和响应速度。但需要注意的是,Unity的所有API都是线程不安全的,所以在使用多线程时,必须采用正确的方式来访问Unity API。 以下是在Unity使用多线程的步骤: 1. 创建新的线程 使用C#的Thread类创建新的线程,如下所示: ``` Thread thread = new Thread(ThreadMethod); thread.Start(); ``` 其,`ThreadMethod`是新线程要执行的方法。 2. 在新线程执行逻辑 在新线程执行复杂的计算或其他需要耗费时间的操作。需要注意的是,在新线程不能直接访问Unity API。 3. 使用线程安全的方式访问Unity API 为了避免访问Unity API时出现线程安全问题,可以使用以下方法: - 使用线程安全的类型,如ConcurrentQueue,来存储需要在主线程处理的数据。 - 使用Unity的主线程调用方法,如`UnityMainThreadDispatcher.Instance().Enqueue()`方法,将需要在主线程执行的代码添加到主线程的执行队列。 以下是一个使用多线程的示例代码: ``` private ConcurrentQueue<float> queue = new ConcurrentQueue<float>(); private void Update() { float value; while (queue.TryDequeue(out value)) { // 在主线程处理数据 Debug.Log(value); } } private void ThreadMethod() { for (float i = 0; i < 10000; i++) { queue.Enqueue(i); } } ``` 在上面的示例代码,我们使用ConcurrentQueue存储需要在主线程处理的数据。然后在Update方法不断地尝试从队列取出数据并在主线程处理。在新线程,我们向队列添加数据。这样可以保证在主线程处理数据,避免了访问Unity API时出现线程安全问题

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值