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

use*_*261 25 c# multithreading unity-game-engine

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

我没有包含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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Pro*_*mer 51

Unity是Thread不安全的,因此他们决定Thread通过添加一种机制来从其他API使用API​​时,无法从另一个API调用API Thread.

这个问题已经被问了很多次,但是没有适当的解决方案/答案.答案通常是"使用插件"或做一些不是线程安全的.希望这将是最后一个.

您通常会在Stackoverflow或Unity的论坛网站上看到的解决方案是简单地使用boolean变量让主线程知道您需要在main中执行代码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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

这种实现允许你调用的函数3个最常用的功能统一:Update,LateUpdateFixedUpdate功能.这也允许你在main中调用run coroutine函数Thread.它可以扩展为能够调用其他Unity回调函数中的函数,如OnPreRenderOnPostRender.

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

void Awake()
{
    UnityThread.initUnityThread();
}
Run Code Online (Sandbox Code Playgroud)

2.Thread从另一个Thread 执行main中的代码:

UnityThread.executeInUpdate(() =>
{
    transform.Rotate(new Vector3(0f, 90f, 0f));
});
Run Code Online (Sandbox Code Playgroud)

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

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

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


void Rotate()
{
    transform.Rotate(new Vector3(0f, 90f, 0f));
}
Run Code Online (Sandbox Code Playgroud)

#2#3样品执行在Update功能.

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

示例是相机跟踪代码.

UnityThread.executeInLateUpdate(()=>
{
    //Your code camera moving code
});
Run Code Online (Sandbox Code Playgroud)

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

这样做的例子是做物理的东西,比如加力Rigidbody.

UnityThread.executeInFixedUpdate(()=>
{
    //Your code physics code
});
Run Code Online (Sandbox Code Playgroud)

6.Thread从另一个线程开始主要的协同程序功能:

UnityThread.executeCoroutine(myCoroutine());

IEnumerator myCoroutine()
{
    Debug.Log("Hello");
    yield return new WaitForSeconds(2f);
    Debug.Log("Test");
}
Run Code Online (Sandbox Code Playgroud)

最后,如果您不需要在LateUpdateFixedUpdate函数中执行任何操作,则应该对以下代码的两行进行注释:

//#define ENABLE_LATEUPDATE_FUNCTION_CALLBACK
//#define ENABLE_FIXEDUPDATE_FUNCTION_CALLBACK
Run Code Online (Sandbox Code Playgroud)

这将提高性能.

  • *"如果没有定义`ENABLE_UPDATE_FUNCTION_CALLBACK`,则不包括该功能."*这就是问题,你所说的没有发生.函数`public static void executeCoroutine(IEnumerator action)`在`#if`块之前,所以如果没有定义`ENABLE_UPDATE_FUNCTION_CALLBACK`,函数`executeCoroutine`仍然存在.我试图说你需要将`#if`12行移到更高的位置,因此它正好在`COROUTINE IMPL`注释之前,这样当符号为**时,**`executeCoroutine`和`executeInUpdate`都不再存在没有定义的. (3认同)

Tho*_*ire 10

在主线程上运行代码但不需要游戏对象 和 的另一种解决方案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);
Run Code Online (Sandbox Code Playgroud)


Pel*_*dez 9

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

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();
           }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

ExecuteOnMainThread.RunOnMainThread.Enqueue(() => {

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

});
Run Code Online (Sandbox Code Playgroud)


Fat*_*tie 6

实际上,有关Unity中线程的许多文章都是完全错误的。

为何如此?

基本事实是,Unity当然完全基于框架。

当您在基于框架的系统中工作时,与常规系统中的线程问题截然不同。

这是完全不同的范例。

(实际上,在许多方面,它要容易得多。)

基于框架的系统上的线程问题与传统系统完全不同。(在某些方面,由于某些概念在基于帧的系统中根本不存在,因此更容易处理。)

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

Thermo.cs
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

因此它将具有在Update中称为的函数,例如

func void ShowThermoValue(float fraction) {
   display code, animates a thermometer
}
Run Code Online (Sandbox Code Playgroud)

每帧只能运行一次,仅此而已。

忘记这个基本事实很容易。

当然,它仅在“主线程”上运行。

(Unity中没有其他内容!只有......个“ Unity线程”!)

但是要记住的关键范例是:它每帧运行一次

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

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

    ... ???
}
Run Code Online (Sandbox Code Playgroud)

注意,当然,这是一个类函数!

您不能以任何方式“接触”到常规的Unity函数,例如ShowThermoValue !!!!!! 脚注1

这是一个完全,完全,毫无意义的概念。您不能“进入” ShowThermoValue。

对于Unity,您看到的任何“与线程相关的”代码示例都试图完全“误导 ”到“线程” 。

没有线程可以到达!

再一次-这真是令人惊讶-如果您只是在Google上搜索所有有关Unity中线程的文章,那么许多文章和帖子在概念上都是完全错误的。

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

您可能将各种科学设备连接到PC和iPhone的机架上,并且新的“温度”值可能会非常频繁地出现……每帧数十次 ……或者也许每隔几秒钟。

那么您如何处理这些值?

这再简单不过了。

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

WTF?您要做的只是设置一个变量?而已?怎么会这么简单?

这是那些异常情况之一:

  1. Unity中有关线程的许多著作完全是错误的。并不是说它有“一个错误”或“需要纠正的问题”。这只是“完全彻底的错误”:在基本范式级别。

  2. 令人惊讶的是,实际方法非常简单。

  3. 非常简单,您可能会认为自己做错了什么。

因此,变量...

[System.Nonserialized] public float latestValue;
Run Code Online (Sandbox Code Playgroud)

从“到达线程”进行设置...

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

    ThisScript.runningInstance.latestValue = f; // done
}
Run Code Online (Sandbox Code Playgroud)

老实说就是这样。

本质上,要成为“ Unity线程”(显然是基于框架)的世界上最出色的专家,除了上述工作之外,没有什么可做的了。

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

真的,就是这样!

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

您只是在显示“最新”值。

最新值可能已被设置为该帧的一次,两次,十倍或一百倍.............但是,您只需显示,ShowThermoValue运行该帧时的值就是什么!

您还能显示什么?

温度计在屏幕上以60fps的速度更新。脚注2

实际上就是那么容易。就这么简单。令人惊讶但真实。


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

正如用户@dymanoid指出的那样(请阅读下面的重要讨论),必须记住,虽然在Unity / C#环境中float是原子的,但其他任何东西(例如Vector3等)都不是原子的。通常(例如此处的示例),您仅传递来自本机插件的计算中的浮点数。但是必须意识到向量等不是原子的。


有时,经验丰富的线程编程人员会与基于框架的系统打交道,原因是:在基于框架的系统中,由跑道和锁定问题引起的大多数问题在概念上都不存在。

在基于框架的系统中,任何游戏项目都应仅基于某个位置设置的“当前值”进行显示或行为。如果您有其他线程到达的信息,只需设置这些值 - 完成

不能 在Unity中有意义地“与主线程交谈”,因为该主线程.............是基于框架的!

除了线程问题之外,连续系统无法与框架范式系统进行有意义的对话

大多数锁定,阻塞和跑道问题在基于框架的范式中都不存在,因为:如果在一个特定的框架中将lastValue设置为十次,一百万次,十亿次,那么您该怎么办?..您只能显示一个值!

想想老式的塑料薄膜。您实际上只有一个框架,仅此而已。如果在一个特定的帧中将lastValue设置为一万亿次,ShowThermoValue将仅显示(在60秒内)运行时获取的一个值。

您要做的就是:将信息保留在某个地方,框架范式系统会根据需要使用该框架。

简而言之就是这样。

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

您所能做

  • 其他计算线程或

  • 从插件线程,

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

而已!


脚注


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

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

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

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

让我们在标题中回答这个问题:

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

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

在此处输入图片说明

当然那是没有意义的。您所能做的就是为下一张照片,下一帧更改某些内容。


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

  • 评论不用于扩展讨论;此对话已[移至聊天](https://chat.stackoverflow.com/rooms/190714/discussion-on-answer-by-fattie-use-unity-api-from-another-thread-or-call-函数)。 (3认同)
  • @Fattie,我还指出,多线程问题不仅与竞争条件有关(原子操作在其中很重要)。还有很多其他的多线程陷阱,例如指令重排序或内存障碍,所有这些陷阱都可以在Unity中轻松实现。因此,Unity **必须解决所有众所周知的多线程问题**。您的声明*“ Unity上的线程问题不同,某些概念根本不存在” *是不正确的并且具有误导性,因此您的回答也是如此。Unity基于.NET运行时(Mono),并且.NET运行时的所有规则均适用。 (3认同)