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

时间:2016-12-26 11:44:59

标签: c# multithreading unity3d

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

我在void getInformation中不包含updatetext.GetComponent<Text>().text = "From server: "+tempMesg;的原因是这个函数在线程中,当我在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);
        }
    }
}

6 个答案:

答案 0 :(得分:45)

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

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

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

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

执行此操作的正确方法之一是创建一个集合,例如List。当您需要在主线程中执行某些操作时,请调用一个函数来存储要在Action中执行的代码。将List Action List复制到Action的本地Action,然后执行List中本地List的代码,然后清除{ {1}}。这可以防止其他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;
        }
    }
}

<强> USAGE

此实现允许您调用 3 最常用的Unity函数中的函数:UpdateLateUpdateFixedUpdate函数。这也允许您在主Thread中调用run coroutine函数。它可以扩展为能够调用其他Unity回调函数中的函数,例如OnPreRenderOnPostRender

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

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

2 。从另一个主题执行主Thread中的代码:

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

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

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函数中执行任何操作,则应在下面对此代码的两行进行注释:

//#define ENABLE_LATEUPDATE_FUNCTION_CALLBACK
//#define ENABLE_FIXEDUPDATE_FUNCTION_CALLBACK

这会提高性能。

答案 1 :(得分:5)

关于Unity中线程的许多著作实际上是完全错误的。

怎么办?

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

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

这是完全不同的范例。

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

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

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

Thermo.cs

enter image description here

因此它将具有在Update中调用的功能,例如

func void ShowThermoValue(float fraction) {
   display code, animates a thermometer
}

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

忘记这个基本事实非常容易。

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

(Unity中什么都没有!只有......“ Unity线程”!)

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

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

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

    ... ???
}

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

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

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

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

没有线程可以进入!

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

让我们说:价值非常频繁且不规则地到达。

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

那您怎么处理这些值?

再简单不过了。

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

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

这是那些异常情况之一:

  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, animates a thermometer
   thermo height = latestValue
}

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

latestValue可能已被设置为该帧的一次,两次,十次或一百倍............但是,您只需显示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电影时代的摄影师,问如何在其中一个镜架上“移动”实际的物体。

enter image description here

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


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

答案 2 :(得分:0)

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

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

public class ExecuteOnMainThread : MonoBehaviour {

    public readonly static ConcurrentQueue<Action> RunOnMainThread = new ConcurrentQueue<Action>();

    void Update()
    {
        if(!RunOnMainThread.IsEmpty())
        {
           while(RunOnMainThread.TryDequeue(out action))
           {
             action.Invoke();
           }
        }
    }

}

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

ExecuteOnMainThread.RunOnMainThread.Enqueue(() => {

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

});

答案 3 :(得分:0)

Use UniRx's multithreading patternUniTaskRxSocket一起。

[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());
}

答案 4 :(得分:0)

另一种在主线程上运行代码但不需要游戏对象和 <View style={styles.cardContent}> //white square </View> 的解决方案是使用 SynchronizationContext

// This code is responsible for centering the square box in the picture:
cardContent: {
    marginTop:19,
    height:188,
    width:188,
    backgroundColor:'#FFFFFF',
    justifyContent:'center',
    marginLeft:'auto',
    marginRight:'auto',
}

答案 5 :(得分:-2)

我总是这样做。希望对您有帮助。

List<string> methodsToCall = new List<string>();

void CallingFromAnotherThread(){
    // do all your stuff here.
    //And when you want to call main thread API add the method name to the list

    methodsToCall.Add("MyMainThreadMethod");
}

void Update(){
    if( methodsToCall.Count > 0 ){
        foreach( string s in methodsToCall ){
            Invoke(s,0f);
        }
    methodsToCall.Clear();
    }
}

void MyMainThreadMethod(){
    // your Unity main thread API here.
}