文章目录

  • 前言
  • 一、unity真的不支持多线程吗?
  • 1.unity中使用多线程
  • 2.unity中多线程的停止
  • 3.unity中使用多线程的问题
  • 二、协同程序
  • 1.协程的使用
  • 2.协程的原理
  • 总结



前言

Unity中的协程,即协同程序,是一个很好用的工具,我们在很多时候都会用到,但协程究竟是怎样的机制?和线程是什么样的区别?下面让我们来一探究竟。


一、unity真的不支持多线程吗?

首先,unity中是支持多线程的。初学unity时看到有些地方说,unity中不支持多线程,就没怎么尝试过。之后进一步学习时才更多的了解了一些,下面通过几个例子来看一下unity中多线程的使用。

1.unity中使用多线程

首先在unity场景中创建一个空物体,创建一个新的C#脚本,在类中定义一个方法,循环打印1,再定义一个线程对象,在Start()生命周期函数中开启线程。

代码如下:

Thread t;
    // Start is called before the first frame update
    void Start()
    {
        t = new Thread(Test);
        t.Start();
    }
    void Test()
    {
        while (true)
        {
            print(1);
        }
    }

将脚本挂载到场景上的空物体,运行,发现可以正常打印出1,也没有报错。

unity 子线程 会抢占主线程调用吗 unity多线程限制_Test

2.unity中多线程的停止

如果做到了这一步,接下来会发现,即使停止运行,程序仍在继续打印1。尝试将线程对象改为后台线程,重新运行:

代码如下:

t.IsBackground = true;

发现仍然不停打印。原因,其实UnityEditor本身就是这个软件程序中的一根线程,当我们在脚本中新开一个线程时,它是和UnityEditor同步执行的,所以我们停止了UnityEditor的运行并不会影响这个线程的执行。解决方案:在脚本的生命周期函数OnDestory()中停止线程,这样停止运行时会调用场景上对象的OnDestory()方法,就会停止其它线程。
代码如下:

void OnDestroy()
    {
        t.Abort();
    }

3.unity中使用多线程的问题

下面我们再通过一个例子看一下,为什么有unity中不支持多线程的这种说法。还是上述脚本,我们改一下Test方法:

代码如下:

Thread t;
    // Start is called before the first frame update
    void Start()
    {
        t = new Thread(Test);
        t.Start();
    }
    void Test()
    {
        while (true)
        {
            print(transform.position);
        }
    }

注意这里将打印结果改为了transform.position,运行,发现报了这样一个错误:

unity 子线程 会抢占主线程调用吗 unity多线程限制_Test_02


提示get_transform是Component类中的一个属性中的get方法,涉及到unity中的一些反射机制,这里不做过多描述,简单来说提示的意思就是我们要打印的transform.position只能在主线程中执行。实际上我们在脚本中新开的线程无法访问绝大部分unity中的对象,都会报这个错误,所以会有unity不支持多线程的这种说法,但实际上这种说法并不完全正确。

虽然新开的线程无法访问unity中的很多对象,但是我们可以将一些复杂的算法计算、网络连接等逻辑抛给一个新的线程去处理,将处理的数据放在公共内存模块中,在unity主线程就可以访问使用了,关于这种案例我们之后再讨论,前面说了很多关于多线程的问题,下面来讲一下unity对于以上问题机制给出的解决方案——协程。


二、协同程序

协同程序简称协程,多在资源、场景异步加载等逻辑时使用,它的使用效果和线程有些类似,都可以在不卡住主程序的情况下开启另一段逻辑的执行,但是它和线程有本质上的区别,协程的执行是在主线程下,而不是在另一个线程中,所以它可以访问unity中的所有对象,而不会出现上述使用线程中遇到的问题。

1.协程的使用

协程的基本使用:声明一个返回值为IEnumerator的方法,用然后用MonoBehaviour中的StartCoroutine()方法开启协程。

代码如下:

void Start()
    {
        //开启协程返回一个协程对象
        Coroutine co = StartCoroutine(Test());
        //关闭协程
        StopCoroutine(co);
    }

    IEnumerator Test()
    {
        while (true)
        {
            print(1);
            //要有一个yield修饰的返回值
            yield return new WaitForSeconds(1);
        }
    }

协程方法是每秒钟打印一次1,这里关于协程的返回值暂时不做过多介绍,之后可能在别的文章中具体说明,读者也可看一下unity官方的描述,下面着重讲一下协程原理。

2.协程的原理

协程本质上包括两部分:
1、协程方法
2、协程调度器

协程方法即返回值为IEnumerator接口类型或其子类的方法,本质上是一个迭代器方法,不了解C#迭代器的读者可以先补充一下这方面的知识。我们进入IEnumerator接口看一下:

public interface IEnumerator
    {
        //表示方法当前执行到的返回值
        object Current { get; }
        //继续执行方法,返回表示是否执行完毕
        bool MoveNext();
        //重置方法的执行位置
        void Reset();
    }

我们不使用MonoBehaviour的StartCoroutine方法,来执行一个协程:

代码如下:

void Start()
    {
        IEnumerator ie = Test();
        print(ie.MoveNext());
        print(ie.Current);

        print(ie.MoveNext());
        print(ie.Current);

        print(ie.MoveNext());
        print(ie.Current);

        print(ie.MoveNext());
        print(ie.Current);
    }

    IEnumerator Test()
    {
        print(1);
        yield return 1;
        print(2);
        yield return "哈哈";
        print(3);
        yield return 3.14;
        print(4);
        yield return gameObject;
    }

unity中的运行结果:

unity 子线程 会抢占主线程调用吗 unity多线程限制_unity 子线程 会抢占主线程调用吗_03

由此可见,所谓协同程序,就是一步步的执行迭代器对象中的MoveNext()方法,调用MoveNext()方法会执行下一个yield return之前的逻辑,并且根据MoveNext()的返回值判断是否全部执行完毕,而通过yield return返回Current成员对象,来判断下一次执行MoveNext()的时机。而这些工作,就是由协同程序的第二部分——协程调度器来实现的。这里的协程调度器是unity引擎实现的,理论上我们是可以自己去实现一个协程调度器的,大家感兴趣的话可以自己实现一个,能进一步加深对协程的理解。

总结

总之,协同程序的存在很大程度上弥补了unity中使用多线程出现的问题,但在原理上与线程有本质上的区别,我们在使用时也要依据情景,选择更适合的方式。另外,文章中并没有过多的阐述协程的使用,如果读者没有这部分的基础需要自己学习一下,之后可能会单独发一篇文章来详细描述协程的使用。