【问题标题】:Cannot access object from main thread after field initialisation字段初始化后无法从主线程访问对象
【发布时间】:2016-03-24 17:13:23
【问题描述】:

我们正在制作一个统一游戏,它使用从 tcp 套接字接收的命令来处理游戏某个校准状态下的动作。 当接收到新字符串时,状态管理器会处理由套接字管理器引发的事件。然后,该状态管理器必须在开始时在字段中引用的游戏对象上触发一个方法。

我们现在面临的问题是处理这些事件的线程无法访问这个对象。我们得到以下错误:

ToString 只能从主线程调用。加载场景时,构造函数和字段初始化器将从加载线程中执行。

在标有这行给出错误的行

Unity 如何使用 EventHandlers 处理线程以及我们如何访问这个对象?

提前致谢!

public class StateManager : MonoBehaviour {

private bool initialized;
private GameObject calibrationController;


void Start(){
    InitializeEventHandler ();
}

void OnLevelWasLoaded(int level) {
    if (level == 3) {
        calibrationController = GameObject.FindGameObjectWithTag("CalibrationController");
        Debug.Log ("calibrationController 1: " + calibrationController);
        calibrationController.GetComponent<CalibrationController> ().NewState += NewCalibrationState;
        calibrationController.GetComponent<CalibrationController>().setupCalibration();
    }
}

private void InitializeEventHandler(){
    GetComponent<GameSocket> ().NewCommand += NewCommandReceived;
}

private void NewCommandReceived(object sender, NewCommandEventArgs e){
    HandleCommandReceived (e.Command);
}

private void NewCalibrationState(object sender, NewCalibrationStateEventArgs e)
{
    HandleNewCalibrationState (e.State);
}

private void HandleCommandReceived(string command){
    switch (command) {
    case "startcalibrationcomplete":
        Debug.Log ("startcalibrationcomplete");
        Debug.Log ("calibrationController 2: " + calibrationController); THIS LINE GIVES THE ERROR !!!
        Debug.Log(GameObject.FindGameObjectWithTag("CalibrationController"));
            break;
    default:
            Debug.Log ("state10");
        break;
    }
}

private void HandleNewCalibrationState(string state){
    switch (state) {
    case "startcalibration":
        GetComponent<GameSocket>().MySend("startcalibration");
        // ...
        break;
    case "animationdone":
        GetComponent<GameSocket>().MySend("animationdone");
        break;
    default:
        Debug.Log ("state10");
        break;
    }
}

对于未来的读者:由于 Unity 的线程不佳,无法从事件处理程序调用游戏对象上的方法。 通过让事件处理程序在一个空的游戏对象上设置数据脚本的属性,我找到了一种解决方法。 可以从任何游戏对象(即在更新周期中)访问此脚本上的数据。

【问题讨论】:

  • (1) Unity 不是多线程的,与多线程无关。 (2) 出于这个原因,每个 Unity 调用(“ToString”和每个其他 Unity 函数) 仅适用于主线程。这是关于 Unity (3) 的一个基本事实,在极少数情况下您需要启动另一个线程(为什么?),因此您必须“返回”主线程来告诉您的应用程序任何事情。 (4) 线程编程困难;如果您正在线程化,您将知道如何“回到”Unity 中的主线程!最后(5)你应该使用UnityEvent(这太棒了)
  • 除 UnityEvent 外,我同意你的所有观点。 UnityEvent 非常慢。非常非常慢。几年前我做了一个基准测试,还用谷歌搜索了我的结果来验证我得到了什么,这不值得。 Unity 知道这一点,他们将根据他们的一位程序员将其删除或在 Unity 6 中重新编写。代表和活动更快。
  • 那个“非常非常慢”的参数只对更新事件有效。如果您的事件偶尔被调用,UnityEvent 很好。它实际上也更安全,因为它们不是真实事件,只是根据请求迭代的委托列表。缓慢的部分来自检查对象是否活着。

标签: c# multithreading unity3d


【解决方案1】:

事实是 Unity 支持 Thread out of box。几年前,他们将 API 更改为 throw 异常,当它们被 另一个 线程调用时。

当您从另一个线程引发一个事件时,该事件会在该线程中被调用不在Unity/Main线程中。所以这意味着当事件被调用时,你仍然不能使用Unity从该事件回调对其具有线程 限制的 API。

我在 Unity 中使用 Threads 进行了大量实验,并得出以下结论:Thread可能可以与 Unity API集成

要在 Unity 中使用 Thread,您必须使用 lock,这是一个可以从 Unity Main Thread 访问的全局变量 boolean strong> 和您创建的 TCP 线程。当您接收来自网络的内容时,您还需要一种调用 Unity API 的方法。使用lockboolean variables 应该实现。然后,您可以从 Update 函数而不是新创建的线程调用 Unity API

不要主线程中创建TCP instance,然后尝试从另一个线程访问它。在另一个线程中创建访问 TCP实例。如果这样做,您将遇到崩溃/冻结或错误。

下面的代码应该向您展示如何在 Unity 中使用 TCP 和 Thread 引发事件。下面代码的目标主线程(更新函数)而不是另一个线程引发事件。您可以根据需要向此代码添加任意数量的事件。

public class TCPRECEIVER: MonoBehaviour
{

    readonly object locker = new object(); //For Locking variables
    bool continueReading = false;
    bool gotNewMessage = false;

    byte[] receivedBytes; //Stores bytes received from the server (will be accessed from Multiple Threads with lock)


    //Event to notify other functions when something is received
    public delegate void newMessageReceieved(byte[] bytesFromServer);
    public static event newMessageReceieved onNewMessageReceieved;


    public void Start()
    {
        receivedBytes = new byte[40];

    }


    void Update()
    {
        //Lock is expensive so make sure that we are still in reading mode before locking
        if (continueReading)
        {
            //lock variables
            lock (locker)
            {
                //Check if there is a new message
                if (gotNewMessage)
                {
                    gotNewMessage = false; //Set to false so that we don't run this again until we receive from server again

                    //Raise the event here if there are subscribers
                    if (onNewMessageReceieved != null)
                    {
                        onNewMessageReceieved(receivedBytes);
                    }
                }
            }
        }
    }

    //Start Reading from Server
    void startReading()
    {
        continueReading = true;

        //Start new Thread
        new System.Threading.Thread(() =>
        {
            //Create Client outside the loo
            System.Net.Sockets.TcpClient tcpClient = new System.Net.Sockets.TcpClient("192.168.1.1", 8090);
            System.Net.Sockets.NetworkStream tcpStream = tcpClient.GetStream();

            //Read Forever until stopReading is called
            while (continueReading)
            {
                byte[] bytesToRead = new byte[tcpClient.ReceiveBufferSize];
                int bytesRead = tcpStream.Read(bytesToRead, 0, tcpClient.ReceiveBufferSize);

                //Check if we received anything from server
                if (bytesRead > 0)
                {
                    //lock variables
                    lock (locker)
                    {
                        //Copy the recived data to the Global variable "receivedBytes"
                        System.Buffer.BlockCopy(bytesToRead, 0, receivedBytes, 0, bytesRead);

                        //Notify the Update function that we got something
                        gotNewMessage = true;
                    }
                }
                System.Threading.Thread.Sleep(1); //So that we don't lock up
            }
        }).Start();
    }

    //Stop reading
    void stopReading()
    {
        continueReading = false;
    }
}

然后您可以通过以下方式订阅和取消订阅来自其他类的事件/消息:

public void OnEnable()
{
    //Subscribe to the event
    TCPRECEIVER.onNewMessageReceieved += receivedBytesFromServer;
}

public void OnDisable()
{
    //Un-Subscribe to the event
    TCPRECEIVER.onNewMessageReceieved -= receivedBytesFromServer;
}


void receivedBytesFromServer(byte []bytesFromServer)
{
  //Do something with the bytes
}

【讨论】:

  • 您的格式使文本难以阅读。当你“强调”一切时,你什么都不强调。这只是不必要的噪音。而且您的代码几乎总是会失败(试图将ReceiveBufferSize 复制到byte[40]?),并且不是很好的示例代码 - 编组结果的基本思想就在那里,但是太多了您的解决方案存在问题,以确保安全。
  • 这不是一个完美的代码。我认为他使用的是 Socket 而不是 TcpClient。这段代码的想法是向他展示如何在从服务器接收到某些内容时通知 Update 函数,以及如何安全地从另一个线程访问变量。他没有发布他的套接字代码,所以我不得不在那里放一些东西以使代码更容易理解。我希望他删除 TcpClient 代码并用他的套接字代码替换它。您可以随时编辑答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-05-15
  • 1970-01-01
  • 2020-12-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多