Unity3D,如何处理事件在正确的线程

本文关键字:线程 处理事件 Unity3D | 更新日期: 2023-09-27 17:51:02

我正在编写Unity3D脚本并使用网络库。当数据准备好时,库发出事件(调用委托)。我的库读取该数据并发出试图访问GameObject的事件,但我得到这些错误

CompareBaseObjectsInternal can only be called from the main thread.
Constructors and field initializers will be executed from the loading thread 
when loading a scene. Don't use this function in the constructor or field 
initializers, instead move initialization code to the Awake or Start function.
ArgumentException: CompareBaseObjectsInternal can only be called from the main 
thread. Constructors and field initializers will be executed from the loading 
thread when loading a scene. Don't use this function in the constructor or field 
initializers, instead move initialization code to the Awake or Start function.

没关系。我想我明白网络消息是在一个单独的线程上处理的,Unity3D试图指出这一点,以及如何从另一个线程操作东西不是线程安全的。

我的问题是:建议用什么方法来处理这个问题?

我想的是,而不是在我的处理程序中直接调用委托,我会做一些对象,引用事件中传递的所有参数,并将它们添加到事件的List<MyEvent>

我将不得不使一个线程安全包装类包含列表,让我添加和删除事件与锁,因为List<>不是线程安全的。

然后我需要在主线程上处理这些事件。我不确定什么是最好的方法。处理网络的类是一个MonoBehavior本身,所以我想我可以让它的Update方法从列表中删除事件并发送它们。

这听起来是个好主意吗?还是有其他更好或更简单的方法来实现这一点?

Unity3D,如何处理事件在正确的线程

最后的结果如下:

public class EventProcessor : MonoBehaviour {
    public void QueueEvent(Action action) {
        lock(m_queueLock) {
            m_queuedEvents.Add(action);
        }
    }
    void Update() {
        MoveQueuedEventsToExecuting();
        while (m_executingEvents.Count > 0) {
            Action e = m_executingEvents[0];
            m_executingEvents.RemoveAt(0);
            e();
        }
    }
    private void MoveQueuedEventsToExecuting() {
        lock(m_queueLock) {
            while (m_queuedEvents.Count > 0) {
                Action e = m_queuedEvents[0];
                m_executingEvents.Add(e);
                m_queuedEvents.RemoveAt(0);
            }
        }
    }
    private System.Object m_queueLock = new System.Object();
    private List<Action> m_queuedEvents = new List<Action>();
    private List<Action> m_executingEvents = new List<Action>();
}

在其他类中的启动代码将其中一个添加到启动它的GameObject中。网络线程通过调用eventProcessor.queueEvent添加Action,上面的Update函数最终在主线程上执行这些动作。我把所有的动作拉到另一个队列中,以尽可能少地保持锁定。

我想这并没有那么难。我只是想知道是否有其他推荐的解决方案。

出于这个原因,我创建了一个简单的类,您可以用一行代码调用它。

你可以这样使用:

public IEnumerator ThisWillBeExecutedOnTheMainThread() {
    Debug.Log ("This is executed from the main thread");
    yield return null;
}
public void ExampleMainThreadCall() {
    UnityMainThreadDispatcher.Instance().Enqueue(ThisWillBeExecutedOnTheMainThread());
}

只要转到https://github.com/PimDeWitte/UnityMainThreadDispatcher并开始使用它,如果你喜欢。

欢呼

我还不能评论,所以我添加一个答案。

你的直觉是正确的。

你不需要一个类来处理锁,只需要在添加到列表时使用锁就足够了。

使用Update方法是推荐的方法,因为它总是在UI线程上触发,所以任何被触发的事件都可以自由地改变显示列表。

如果你想的话,你可以把接收数据的任务和触发事件的任务分开,用一个单独的MonoBehaviour来处理事件的调度。

你可以创建线程,但是,不要在单独的线程中对GameObject或GameObject的任何组件做任何事情。

但是我真的不明白为什么你需要使用它,为什么你不能直接使用协程?

void SomeMethod() 
{
       StartCoroutine(CallNetworkCode());
       // ...keep going...
}
IEnumerator CallNetworkCode()
{
    bool done = false;
    var request = LibraryNetwork.DoSomeRequest(() => { done = true; });
    while (!done)
    {
        yield return 0; // wait for next frame
    }
    if (request.result != null)
        Debug.Log("ha!");
}

或者直接将结果解析代码分配给委托