SetActive() 只能从主线程调用

Posted

技术标签:

【中文标题】SetActive() 只能从主线程调用【英文标题】:SetActive() can only be called from the main thread 【发布时间】:2018-12-24 18:06:39 【问题描述】:

我被这个问题困扰了 3 天,我做了很多研究,但找不到任何答案,这里是正在发生的事情的简要说明,尝试使用 Firebase 数据库和使用 Unity3D 进行身份验证,这里步骤是:

第一个用户登录,如果成功,它将从数据库中获取用户的数据,然后应该停用身份验证面板并激活用户面板。

尝试 SetActive 面板时出现此错误。

SetActive 只能从主线程调用。 加载场景时,构造函数和字段初始化程序将从加载线程中执行。 不要在构造函数或字段初始化程序中使用此函数,而是将初始化代码移至 Awake 或 Start 函数。 UnityEngine.GameObject:SetActive(Boolean)

public void SignInWithEmail()

    auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task => 
      DatabaseReference.GetValueAsync().ContinueWith(task => 

        //here after successful signing in, it gets the data from the Database
        //and after that it should activate the user panel
        //and deactivate the authentication panel

        //HERE IS THE PROBLEM 
        userPanel.SetActive(true);
        authPanel.SetActive(false);
    
  

我没有尝试加载另一个场景或其他任何内容。

如果需要,我可以提供更多信息

【问题讨论】:

UI 元素只能在 UI 线程中修改,你是在另一个线程中调用 SetActive。 @Luis 那么如何在主线程中调用它呢? (我没有使用任何线程) 【参考方案1】:

所以我的答案与 Milod 的 接受的答案非常相似,但有点不同,因为我花了一段时间才把头绕在他的周围,尽管他的仍然有效。

    问题: 通常,您的所有代码都在 Unity 中的单个线程上运行,因为 Unity 是单线程的, 然而,当使用像 Firebase 这样需要回调的 API 时,回调函数将由新线程处理。 这可能会导致竞争条件,尤其是在 Unity 等单线程引擎上。

    解决方案(来自 Unity): 从 Unity 2017.X 开始,Unity 现在需要更改 UI 组件才能在主线程(即使用 Unity 启动的第一个线程)上运行。

    有什么影响?: 主要是修改 UI 的调用...

    gameObject.SetActive(true);  // (or false)
    textObject.Text = "some string" // (from UnityEngine.UI)
    

    这与您的代码有何关系:

public void SignInWithEmail() 
    // auth.SignInWithEmailAndPasswordAsyn() is run on the local thread, 
    // ...so no issues here
    auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task => 

     // .ContinueWith() is an asynchronous call 
     // ...to the lambda function defined within the  task=>  
     // and most importantly, it will be run on a different thread, hence the issue
      DatabaseReference.GetValueAsync().ContinueWith(task => 

        //HERE IS THE PROBLEM 
        userPanel.SetActive(true);
        authPanel.SetActive(false);
    
  


    建议的解决方案: 对于那些需要回调函数的调用,比如...
DatabaseReference.GetValueAsync()

...你可以...

将它们发送到设置为在该初始线程上运行的函数。 ...它使用队列来确保它们将按照添加的顺序运行。 ...并按照 Unity 团队建议的方式使用单例模式。

实际解决方案

    将下面的代码放置在您的场景中,该游戏对象将始终启用,这样您就有了一个... 始终在本地线程上运行 可以发送这些回调函数在本地线程上运行。
using System;
using System.Collections.Generic;
using UnityEngine;

internal class UnityMainThread : MonoBehaviour

    internal static UnityMainThread wkr;
    Queue<Action> jobs = new Queue<Action>();

    void Awake() 
        wkr = this;
    

    void Update() 
        while (jobs.Count > 0) 
            jobs.Dequeue().Invoke();
    

    internal void AddJob(Action newJob) 
        jobs.Enqueue(newJob);
    


    现在从您的代码中,您可以简单地调用...

     UnityMainThread.wkr.AddJob();
    

...使您的代码易于阅读(和管理),如下所示...

public void SignInWithEmail() 
    auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task => 

      DatabaseReference.GetValueAsync().ContinueWith(task => 
        UnityMainThread.wkr.AddJob(() => 
            // Will run on main thread, hence issue is solved
            userPanel.SetActive(true);
            authPanel.SetActive(false);            
        )

    
  

【讨论】:

您的解决方案有效。谢谢。另外,您能否确认这不会对性能造成额外开销? 嗨,Soorya。不应该有任何额外的开销。这里的问题是谁(或什么)来做这项工作。内部受信任的人(或线程)(我们知道我们将能够根据需要影响他们),而不是未经审查的“其他人”。 ..但要完成的工作量保持不变。请原谅这个类比,希望它有助于澄清问题。【参考方案2】:

所以基本上 UI 元素需要在主线程中修改,我找到了这个脚本,它会在主线程中执行你的函数,只需将你的函数放在 CoroutineEnqueue 到脚本(UnityMainThreadDispatcher)。 (您需要在场景中添加一个对象并将 MainThreadDispathcer 脚本添加到其中)

这是我的函数的外观:

public void SignInWithEmail()

    auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task => 
     DatabaseReference.GetValueAsync().ContinueWith(task => 
         //Here's the fix
         UnityMainThreadDispatcher.Instance().Enqueue(ShowUserPanel());
    
  



public IEnumerator ShowUserPanel()

    uiController.userPanel.panel.SetActive(true);
    uiController.authPanel.SetActive(false);
    yield return null;

这是在 Main Thead 中运行的脚本

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


public class UnityMainThreadDispatcher : MonoBehaviour 

private static readonly Queue<Action> _executionQueue = new Queue<Action>();


/// <summary>
/// Locks the queue and adds the IEnumerator to the queue
/// </summary>
/// <param name="action">IEnumerator function that will be executed from the main thread.</param>
public void Enqueue(IEnumerator action) 
    lock (_executionQueue) 
        _executionQueue.Enqueue (() => 
            StartCoroutine (action);
        );
    


/// <summary>
/// Locks the queue and adds the Action to the queue
/// </summary>
/// <param name="action">function that will be executed from the main thread.</param>
public void Enqueue(Action action)

    Enqueue(ActionWrapper(action));

IEnumerator ActionWrapper(Action a)

    a();
    yield return null;



private static UnityMainThreadDispatcher _instance = null;

public static bool Exists() 
    return _instance != null;


public static UnityMainThreadDispatcher Instance() 
    if (!Exists ()) 
        throw new Exception ("UnityMainThreadDispatcher could not find the UnityMainThreadDispatcher object. Please ensure you have added the MainThreadExecutor Prefab to your scene.");
    
    return _instance;


void Awake() 
    if (_instance == null) 
        _instance = this;
        DontDestroyOnLoad(this.gameObject);
    


public void Update() 
    lock(_executionQueue) 
        while (_executionQueue.Count > 0) 
            _executionQueue.Dequeue().Invoke();
        
    


void OnDestroy() 
        _instance = null;



【讨论】:

src: github.com/PimDeWitte/UnityMainThreadDispatcher【参考方案3】:

请注意,Firebase 现在有一个很好的 ContinueWithOnMainThread 扩展方法,它比其他建议的答案更优雅地解决了这个问题:

using Firebase.Extensions;
public void SignInWithEmail() 
    // This code runs on the caller's thread.
    auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task => 
      // This code runs on an arbitrary thread.
      DatabaseReference.GetValueAsync().ContinueWithOnMainThread(task => 
        // This code runs on the Main thread. No problem.
        userPanel.SetActive(true);
        authPanel.SetActive(false);
    
  
```

【讨论】:

为其他人标记 using Firebase.Extensions 在此代码 sn-p 中很重要 - 否则,它使用 system.Threading.Tasks' 未扩展版本,只有 ContinueWith

以上是关于SetActive() 只能从主线程调用的主要内容,如果未能解决你的问题,请参考以下文章

- 必须仅从主线程调用[UIApplication委托]

GMSThreadException' 原因:'API 方法必须从主线程调用'

即使从主线程调用更新,UI 也不会更新

在 Apple 的 Cocoa API 中,为啥从主线程调用 NSApplicationMain 很重要?

为啥从主线程调用时,`std::promise::set_value` 会抛出错误?

如何从主线程超时java线程?