Unity多线程

本文内容来源于Youtube上的一个Unity教学视频:Tutorial Unity & Multithreading

1. Unity多线程的特点

Unity可以使用大部分.NET库,包括Threading命名空间下的线程操作,但Unity中的多线程是有限制的,那就是:不能在子线程中修改Unity Object。这里的Object包括transform、gameobject等,但不包括值类型比如:Vector3。这种限制的原因是多线程的一个基本问题,就是同时读写一块儿内存空间,将会造成灾难性的后果。假设我们可以在一个子线程中修改对象的某个组件,比如transform,以Unity这种脚本挂载在对象下的编辑方式,谁知道那个脚本就引用这个对象并修改了它的transform组件,这样上述多线程的基本问题就很容易会发生。所以我觉得Unity的编辑和工作方式是造成这个限制的一个重要原因。

2. 如何在Unity中科学地使用多线程

(1)基本的线程同步

那么应该如何正确地在Unity中使用多线程呢?首先,Coroutine就是一个可替代的方案,不过它本质上并不是多线程,所以这里就不讨论了。当我们需要在游戏进行中执行一个非常耗时的操作,如果放在主线程中会造成卡顿时,可以考虑创建一个子线程来执行这部分操作。我们必须首先遵循线程同步的基本原则,看下面代码:

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

using System.Threading;

public class tryMultiThreads : MonoBehaviour {

    private Thread t;
    private static readonly object tLock = new object();
    string s;

	void Start () {
        t = new Thread(writeDebugLog);
        t.Start();
	}
	
	void Update ()
    {
        lock (tLock)
        {
            s = "Main";
            Debug.Log("Main Thread s: " + s);
        }
    }

    private void writeDebugLog()
    {
        for (int i = 0; i < 5; i++)
        {
            Thread.Sleep(2000);

            lock (tLock)
            {
                s += "Child";
                Debug.Log("Child Thread s: " + s);
            }
        }
    }
}

这段代码的输出结果应该是这样的:

Unity多线程_第1张图片

上述代码创建了一个子线程来修改并打印字符串s,在主线程(Update)中同样执行了类似代码。如果不使用lock进行同步,那么两个线程中对s的读写可能会同时发生,那么输出结果将会不可控制。因此Unity多线程首先要遵循基本的线程同步原则。

(2)回调队列

那么当子线程执行的耗时操作将会导致Unity Object的更改,该如何实现呢?一个简单的办法是对需要更改的Unity Object的组件,进行一个拷贝,子线程中对这个组件拷贝进行修改(前提这个组件是可以在子线程中修改的,比如Vector3),然后在主线程中使用这份拷贝对Unity Object进行赋值。曾经在一本书上看过使用静态变量存储游戏的状态变量,再使用多线程进行网络数据接收并修改这些静态变量,最后赋值给GameObject。这里面静态变量就相当于上述“拷贝”。然而,这样的方法显然是比较麻烦的,如果涉及的变量太多会造成很大的空间冗余,而且在开发时还要记得这么多冗余变量...因此,我们采用一种“回调队列”(不知道应该叫啥自己起的)的方法,先看下面的代码:

using System.Collections.Generic;
using UnityEngine;

using System.Threading;
using System;

public class ThreadQueuer : MonoBehaviour {

    //回调队列
    List functionsToRunInMainThread = new List();

	void Start ()
    {
        Debug.Log("Start() ----- Start");

        startThreadedFunction(SlowFunctionThatDoesInUnity);

        Debug.Log("Start() ----- Done");
	}
	
	void Update ()
    {
		if (functionsToRunInMainThread.Count>0)
        {
            Action func = functionsToRunInMainThread[0];
            functionsToRunInMainThread.RemoveAt(0);

            func();
        }
	}

    //创建一个子线程执行
    void startThreadedFunction(Action someFunction)
    {
        Thread t = new Thread(new ThreadStart(someFunction));
        t.Start();
    }

    //将一个回调加入调用队列
    void QueueMainThreadFunction(Action someFunction)
    {
        functionsToRunInMainThread.Add(someFunction);
    }

    void SlowFunctionThatDoesInUnity()
    {
        Thread.Sleep(2000); //SlowFunction
        
        QueueMainThreadFunction(() => {
            this.transform.position = new Vector3(1, 1, 1); // 注意:不能在子线程中执行!
            Debug.Log("The results of the child thread are being applied to Unity GameObject safely!");
        });
    }
}

创建一个GameObject然后挂载上面的脚本,运行后控制台的输出应该是这样的:

Unity多线程_第2张图片

控制台首先打印出前两条消息,过了2秒后打印最后一条消息,并且将cube的position设置为(1,1,1)。回调队列的基本想法就是巧妙利用C#委托,将耗时操作与操作后对Unity Object的修改分开,在子线程中执行前者,将后者放在一个委托中在主线程执行。这种方法类似于异步操作中的“回调”(Callback),并且通过一个队列来管理这些回调,放在主线程中依次执行。

3. 总结

对于Unity中多线程的使用,该教程的主讲人给出了一些建议:

(1)尽量不用多线程,能不用就不用...

(2)尽量使用Coroutine代替,因为它更安全。

(3)如果使用多线程,子线程中执行的操作应该是耗时的,并且能够自行终止的,不要在子线程中运行像while (true)这种无法退出的代码。

你可能感兴趣的:(Unity)