繁体   English   中英

从另一个线程使用 Unity API 或调用主线程中的函数

[英]Use Unity API from another Thread or call a function in the main Thread

我的问题是我尝试使用 Unity 套接字来实现一些东西。 每次,当我收到一条新消息时,我都需要将其更新为更新文本(它是一个 Unity 文本)。 但是,当我执行以下代码时,每次都不会调用 void 更新。

我不包括的原因updatetext.GetComponent<Text>().text = "From server: "+tempMesg; 在 void getInformation 中,这个函数在线程中,当我在 getInformation() 中包含它时,它会出现错误:

getcomponentfastpath can only be called from the main thread

我认为问题是我不知道如何在 C# 中同时运行主线程和子线程? 或者可能还有其他问题。

这是我的代码:

using UnityEngine;
using System.Collections;
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine.UI;


public class Client : MonoBehaviour {

    System.Net.Sockets.TcpClient clientSocket = new System.Net.Sockets.TcpClient();
    private Thread oThread;

//  for UI update
    public GameObject updatetext;
    String tempMesg = "Waiting...";

    // Use this for initialization
    void Start () {
        updatetext.GetComponent<Text>().text = "Waiting...";
        clientSocket.Connect("10.132.198.29", 8888);
        oThread = new Thread (new ThreadStart (getInformation));
        oThread.Start ();
        Debug.Log ("Running the client");
    }

    // Update is called once per frame
    void Update () {
        updatetext.GetComponent<Text>().text = "From server: "+tempMesg;
        Debug.Log (tempMesg);
    }

    void getInformation(){
        while (true) {
            try {
                NetworkStream networkStream = clientSocket.GetStream ();
                byte[] bytesFrom = new byte[10025];
                networkStream.Read (bytesFrom, 0, (int)bytesFrom.Length);
                string dataFromClient = System.Text.Encoding.ASCII.GetString (bytesFrom);
                dataFromClient = dataFromClient.Substring (0, dataFromClient.IndexOf ("$"));
                Debug.Log (" >> Data from Server - " + dataFromClient);

                tempMesg = dataFromClient;

                string serverResponse = "Last Message from Server" + dataFromClient;

                Byte[] sendBytes = Encoding.ASCII.GetBytes (serverResponse);
                networkStream.Write (sendBytes, 0, sendBytes.Length);
                networkStream.Flush ();
                Debug.Log (" >> " + serverResponse);

            } catch (Exception ex) {
                Debug.Log ("Exception error:" + ex.ToString ());
                oThread.Abort ();
                oThread.Join ();
            }
//          Thread.Sleep (500);
        }
    }
}

Unity 不是Thread安全的,因此他们决定通过添加一种机制来在另一个Thread使用其 API 时抛出异常,从而使从另一个Thread调用他们的 API 变得不可能。

这个问题已经被问过很多次了,但没有一个正确的解决方案/答案。 答案通常是“使用插件”或做一些非线程安全的事情。 希望这将是最后一次。

您通常会在 Stackoverflow 或 Unity 论坛网站上看到的解决方案是简单地使用一个boolean变量让主线程知道您需要在主Thread执行代码。 这是不对的,因为它不是线程安全的,并且不能让您控制提供要调用的函数。 如果有多个Threads需要通知主线程怎么办?

您将看到的另一个解决方案是使用协程而不是Thread 行不通的。 对套接字使用协程不会改变任何东西。 您仍然会遇到冻结问题。 您必须坚持使用Thread代码或使用Async

执行此操作的正确方法之一是创建一个集合,例如List 当您需要在主线程中执行某些操作时,调用一个函数来存储要在Action执行的代码。 复制ListAction到本地ListAction然后从本地执行的代码ActionList ,然后清除List 这可以防止其他Threads不得不等待它完成执行。

您还需要添加一个volatile boolean来通知Update函数在List中有等待执行的代码。 List复制到本地List时,应该将其包裹在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 函数中的函数: UpdateLateUpdateFixedUpdate函数。 这也允许您在主Thread调用运行协程函数。 它可以扩展为能够调用其他 Unity 回调函数中的函数,例如OnPreRenderOnPostRender

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

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

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

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

这会将 scipt 附加到的当前对象旋转到 90 度。 您现在可以在另一个Thread使用 Unity API( transform.Rotate )。

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

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


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

#2#3示例在Update函数中执行。

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

这方面的示例是相机跟踪代码。

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

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

在进行物理操作时的示例,例如向Rigidbody增加力。

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

6.从另一个线程启动主Thread的协程函数:

UnityThread.executeCoroutine(myCoroutine());

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

最后,如果您不需要在LateUpdateFixedUpdate函数中执行任何FixedUpdate ,您应该在下面注释此代码的两行:

//#define ENABLE_LATEUPDATE_FUNCTION_CALLBACK
//#define ENABLE_FIXEDUPDATE_FUNCTION_CALLBACK

这将提高性能。

我一直在使用这个解决方案来解决这个问题。 使用此代码创建脚本并将其附加到游戏对象:

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

public class ExecuteOnMainThread : MonoBehaviour {

    public static readonly ConcurrentQueue<Action> RunOnMainThread = new ConcurrentQueue<Action>();
        
    void Update()
    {
        if(!RunOnMainThread.IsEmpty)
        {
           while(RunOnMainThread.TryDequeue(out var action))
           {
             action?.Invoke();
           }
        }
    }
}

然后,当您需要在主线程上调用某些内容并从应用程序中的任何其他函数访问 Unity API 时:

ExecuteOnMainThread.RunOnMainThread.Enqueue(() => {

    // Code here will be called in the main thread...

});

很多关于 Unity 线程的文章都是不正确的。

为何如此?

当然,Unity 完全基于框架。

当您在基于框架的系统中工作时,线程问题完全不同。

基于框架的系统上的线程问题完全不同。 (实际上,处理起来往往容易得多。)

假设您有一个 Unity 温度计显示屏,显示一些值

Thermo.cs

在此处输入图片说明

所以它将有一个在更新中调用的函数,比如

func void ShowThermoValue(float fraction) {
   display code to show the current thermometer value
}

回想一下,Unity 中的“更新”功能仅表示“每帧运行一次”。

每帧只运行一次,就是这样。

(当然,它只在“主线程”上运行。Unity 中没有其他东西!只有……“Unity 线程”!)

在其他地方,也许在“IncomingData.cs”中,您将拥有一个处理“新值已到达”概念的函数:

[MonoPInvokeCallback(typeof(ipDel))]
public static void NewValueArrives(float f) {

    ... ???
}

请注意,当然,这是一个类函数! 还能是什么?

您无法“进入”正常的 Unity 功能。 (例如 ShowThermoValue。)那将毫无意义——它只是一个每帧运行一次的函数。 脚注 1

假设:值非常频繁且不规则地到达。

图像您有某种科学设备(可能是红外温度计)连接到 PC 机架

这些电子设备经常提供新的“温度”值。 让我们说每帧几十次。

因此,“NewValueArrives”每秒被调用 100 次。

那么你如何处理这些值呢?

再简单不过了。

在到达值线程中,您要做的就是 ....... 等待它 ...... 在组件中设置一个变量!!

跆拳道? 你所做的只是设置一个变量 就是这样? 怎么会这么简单?

这是其中一种不寻常的情况:

  1. Unity 中关于线程的大部分内容,简单地说,完全没有希望。

  2. 令人惊讶的是,实际方法极其简单

  3. 太简单了,你可能会认为你做错了什么!!

所以有变量...

[System.Nonserialized] public float latestValue;

从“到达线程”设置它......

[MonoPInvokeCallback(typeof(ipDel))]
public static void NewValueArrives(float f) {

    ThisScript.runningInstance.latestValue = f; // done
}

老实说就是这样。

从本质上讲,要成为“Unity 中的线程”方面的世界上最伟大的专家——这显然是基于框架的——除了上述之外别无他法。

每当ShowThermoValue被调用时,每一帧 ...................... 只需显示该值!

真的,就是这样!

[System.Nonserialized] public float latestValue;
func void ShowThermoValue() { // note NO arguments here!
   display code, draws a thermometer
   thermo height = latestValue
}

您只是显示“最新”值。

latestValue 可能在该帧中设置了一次、两次、十次或一百次…………但是,当ShowThermoValue运行该帧时,您只需显示任何值!

你还能展示什么?

温度计在屏幕上以 60fps 的速度更新,因此您可以显示最新值。 脚注 2

其实就是这么简单。 就是这么简单。 令人惊讶但真实。


# (除了关键 - 不要忘记 vector3 等在 Unity/C# 中不是原子的)

正如用户@dymanoid 所指出的(阅读下面的重要讨论),重要的是要记住,虽然浮点数在 Unity/C# 环境中是原子的,但其他任何东西(例如 Vector3 等)都不是原子的。 通常(如此处的示例中所示),您只会通过来自本地插件、温度计等的计算传递浮点数。但必须注意矢量等不是原子的。


有时,有经验的线程程序员会与基于框架的系统纠缠不清,因为:在基于框架的系统中,大多数由跑道和锁定问题引起的问题……在概念上并不存在。

在基于帧的系统中,任何游戏项目都应该简单地根据某个“当前值”进行显示或行为,该值设置在某处。 如果您有来自其他线程的信息,只需设置这些值 - 您就完成了

无法在 Unity 中有意义地“与主线程对话” ,因为该主线程 ................ 是基于框架的!

大多数锁定、阻塞和赛道问题在基于帧的范式中都不存在,因为:如果你在一个特定的帧中设置 latestValue 十次、一百万次、十亿次……你能做什么? .. 在该帧中您只能显示一个值!

想想老式的塑料薄膜。 你真的只有......一个框架,就是这样。 如果在一个特定的帧中将 latestValue 设置为一万亿次,ShowThermoValue 将简单地显示(在 60 秒内)它在运行时获取的一个值。

您要做的就是:将信息留在某处,如果需要,框架范式系统将在该框架期间使用该信息。

简而言之就是这样。

因此,大多数“线程问题”在 Unity 中都消失了。

你可以做的一切

  • 其他计算线程或

  • 从插件线程,

只是游戏可能使用的“下降值”。

就是这样!

让我们考虑问题标题...

你如何“......在主线程中调用一个函数”

完全没有意义 Unity 中的“函数”只是帧引擎每帧运行一次的函数。

你不能在 Unity 中“调用”任何东西。 帧引擎每帧运行一次很多东西(很多东西)。

请注意,线程确实是完全不相关的。 如果 Unity 运行 10 亿个线程,或者使用量子计算,它就不会产生任何影响。

您不能在基于框架的系统中“调用函数”。

幸运的是,采用的方法非常简单,您只需设置值,基于帧的函数可以在需要时查看这些值! 这真的很容易。


脚注


1你怎么能? 作为一个思想实验,忘记你在不同线程上的问题。 ShowThermoValue 由帧引擎每帧运行一次。 你不能以任何有意义的方式“调用”它。 与普通的 OO 软件不同,你不能,比如说,实例化一个类的实例(一个组件??无意义的)并运行那个函数 - 这完全没有意义。

在“普通”线程编程中,线程可以前后对话等等,这样做时您会担心锁定、赛道等问题。 但这在基于框架的 ECS 系统中毫无意义 没有什么可以“交谈”的。

假设 Unity 实际上是多线程的!!!! 因此,Unity 人员让所有引擎都以多线程方式运行。 它不会有任何区别- 您无法以任何有意义的方式“进入” ShowThermoValue ! 它是一个组件,框架引擎每帧运行一次,仅此而已。

所以 NewValueArrives不在任何地方——它是一个类函数!

让我们回答标题中的问题:

“从另一个线程使用 Unity API 还是在主线程中调用函数?”

这个概念是>>完全没有意义的<<。 Unity(与所有游戏引擎一样)是基于帧的。 在主线程上没有“调用”函数的概念。 打个比方:这就像赛璐珞电影时代的摄影师询问如何其中一个帧实际“移动”某些东西。

在此处输入图片说明

当然那是没有意义的。 你所能做的就是为下一张照片、下一帧改变一些东西。


2我指的是“到达值线程”……事实上! NewValueArrives 可能会或可能不会在主线程上运行!!!! 它可能在插件的线程上运行,也可能在其他线程上运行! 当您处理 NewValueArrives 调用时,它实际上可能是完全单线程的! 只是没关系! 在基于框架的范例中,您所做的以及您所能做的就是“留下一些信息”,组件(例如 ShowThermoValue)可能会使用它们认为合适的信息。

在主线程上运行代码但不需要游戏对象和MonoBehavior另一种解决方案是使用SynchronizationContext

// On main thread, during initialization:
var syncContext = System.Threading.SynchronizationContext.Current;

// On your worker thread
syncContext.Post(_ =>
{
    // This code here will run on the main thread
    Debug.Log("Hello from main thread!");
}, null);

将 UniRx 的多线程模式UniTaskRxSocket一起使用。

[SerializeField] private Text m_Text;

async UniTaskVoid Connect() {
    IPEndPoint endPoint = new IPEndPoint(IPAddress.IPv6Loopback, 12345);

    // Create a socket client by connecting to the server at the IPEndPoint.
    // See the UniRx Async tooling to use await 
    IRxSocketClient client = await endPoint.ConnectRxSocketClientAsync();

    client.ReceiveObservable
        .ToStrings()
        .ObserveOnMainThread()
        .Subscribe(onNext: message =>
    {
        m_Text.text = message;
    }).AddTo(this);

    // Send a message to the server.
    client.Send("Hello!".ToByteArray());
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM