Unity应用架构设计(10)——绕不开的协程和多线程(Part 1)

简介:

在进入本章主题之前,我们必须要了解客户端应用程序都是单线程模型,即只有一个主线程(Main Thread),或者叫做UI线程,即所有的UI控件的创建和操作都是在主线程上完成的。而服务器端应用程序,也就是我们常见的Web应用程序往往是多线程的,故用户A访问势必不会影响用户B的访问过程。所以对于Web应用而言,多线程的数据同步和并发的管理往往是个头疼的问题。那么对于客户端应用程序而言,就一个人使用,还要需要考虑多线程吗?

是否需要多线程?

这是个好问题,从设备的硬件上,这已不是瓶颈:

学过操作系统的同学肯定知道CPU是真正的处理大脑,在单核的CPU年代,在某一时刻CPU只能处理一个线程,通过CPU的调度来实现在不同线程间切换工作。由于CPU调度的时间很快,所以给人造成并发的假象。
随着硬件的提升,多核CPU已经是常态化了。比如双核CPU而言,某一时刻可以有2个线程并行计算。

所以,是否需要在客户端使用多线程技术,还是取决于你的应用的复杂度:

  • 如果你的应用不需要一些耗时的操作,比如网络请求,IO操作,AI等,那么尽量不要使用多线程,因为跨线程访问UI控件是禁止的,并且数据同步问题往往也是很棘手的,很容易滥用lock导致主线block或者deadlock。
  • 反之,如果应用程序很复杂,那么势必在需要去分担主线程的压力,那么使用异步线程是个很好的主意。
  • 同时,我们也不能滥用线程,过多的使用线程会造成CPU运算的下降,建议使用线程池ThreadPool或者利用GC来回收线程。

协程的内部原理

回到本文的主题,对于Unity应用程序而言,还提供了另外一种『异步方式』:CoroutineCoroutine也就是协程的意思,只是看起来像多线程,它实际上并不是,还是在主线程上操作。

Coroutine实际上由IEnumerator接口以及一个或者多个的yield语句构成的迭代器(iterator)块构成。

枚举器接口 IEnumerator 包含3个方法:

  • Current:返回集合当前位置的对象
  • MoveNext:把枚举器位置移到集合的下一个元素,它返回一个bool值,表示新的位置是否超过索引
  • Reset:把位置重置为初始状态

yield是个比较晦涩的技术,原因是编译器帮我们做了太多的工作(CompilerGenerate),导致我们无法理解到内部的实现。如果你去翻阅汉英词典,你会对yield一头雾水。我个人倾向将其翻译成中断产出比较好,这也是yield单词包含的意思,我下面也会阐述为什么要翻译成这两个意思。

深究yield之前,我觉得应该略微了解一下为什么我们能foreach遍历一个数组?

原因很简单,数组Array它是一个可枚举的类(enumerable),一个可枚举类提供了一个枚举器(enumerator),枚举器可以依次访问数组里的元素,也就是之前提过的Current属性返回集合当前位置的对象。所以,我可以模拟foreach的实现,实际上foreach内部实现也大致相似。

static void Main(string[] args)
{
    string[] animals = {"dog", "cat", "pig"};
    //获取枚举器
    var ie = animals.GetEnumerator();
    //移到下一项,默认的index=-1
    while (ie.MoveNext())
    {
        //获得当前项
        Console.WriteLine(ie.Current);
    }
    Console.ReadLine();
}

假设你是个C#新手,你得好好消化一下上述的逻辑,因为这是拨开迷雾的第一层:了解为什么能够枚举一个集合。当然我们也可以创建自己的可被枚举的类,需要为它提供自定义的枚举器,只需实现IEnumerator接口即可。值得注意的事,自建的可枚举类同时也要实现IEnumerable接口,该接口只提供一个方法:GetEnumerator(),用来返回枚举器。

创建自定义的枚举类AnimalSet

class AnimalSet : IEnumerable
{
    private readonly string[] _animals = {"the dog", "the pig", "the cat"};
    public IEnumerator GetEnumerator()
    {
        return new AnimalEnumerator(_animals);
    }
}

需要为AnimalSet提供自定义的枚举器AnimalEnumerator

class AnimalEnumerator : IEnumerator
{
    private string[] _animals;
    private int _index = -1;

    public AnimalEnumerator(string[] animals)
    {
        _animals=new string[animals.Length];

        for (var i = 0; i < animals.Length; i++)
        {
            _animals[i] = animals[i];
        }
    }

    public bool MoveNext()
    {
        _index++;
        return _index<_animals.Length;
    }

    public void Reset()
    {
        _index = -1;
    }

    public object Current
    {
        get { return _animals[_index]; }
    }
}

你可能会觉得奇怪,这和yield又有什么关系呢?要解惑yield这是第二个阶段:能知道枚举器是怎样工作的。

如果你很清楚上诉两个阶段的内部原理之后,要理解Unity中的Coroutine是非常简单的,你会了解为什么它是伪的“多线程”。
这是一段非常普通的代码,司空见惯。

void Start()
{
    StartCoroutine(MyEnumerator());
    Debug.Log("finish");
}

private IEnumerator MyEnumerator()
{
    Debug.Log("wait for 1s");
    yield return new WaitForSeconds(1);
    Debug.Log("wait for 2s");
    yield return new WaitForSeconds(2);
    Debug.Log("wait for 3s");
    yield return new WaitForSeconds(3);
}

注意到MyEnumerator方法的放回类型了吗?没错,返回的就是枚举器,你会疑问,你没有定义一个枚举器并且实现了IEnumerator接口啊!别急,问题就出在yield上,C#为了简化我们创建枚举器的步骤,你想想看你需要先实现IEnumerator接口,并且实现Current,MoveNextReset步骤。C#从2.0开始提供了有yield组成的迭代器块。编译器会自动更具迭代器块创建了枚举器。不信,反编译看看:

public class Test : MonoBehaviour
{
    private IEnumerator MyEnumerator()
    {
        UnityEngine.Debug.Log("wait for 1s");
        yield return new WaitForSeconds(1f);
        UnityEngine.Debug.Log("wait for 2s");
        yield return new WaitForSeconds(2f);
        UnityEngine.Debug.Log("wait for 3s");
        yield return new WaitForSeconds(3f);
    }

    private void Start()
    {
        base.StartCoroutine(this.MyEnumerator());
        UnityEngine.Debug.Log("finish");
    }

    [CompilerGenerated]
    private sealed class <MyEnumerator>d__1 : IEnumerator<object>, IEnumerator, IDisposable
    {
        private int <>1__state;
        private object <>2__current;
        public Test <>4__this;

        [DebuggerHidden]
        public <MyEnumerator>d__1(int <>1__state)
        {
            this.<>1__state = <>1__state;
        }

        private bool MoveNext()
        {
            switch (this.<>1__state)
            {
                case 0:
                    this.<>1__state = -1;
                    UnityEngine.Debug.Log("wait for 1s");
                    this.<>2__current = new WaitForSeconds(1f);
                    this.<>1__state = 1;
                    return true;

                case 1:
                    this.<>1__state = -1;
                    UnityEngine.Debug.Log("wait for 2s");
                    this.<>2__current = new WaitForSeconds(2f);
                    this.<>1__state = 2;
                    return true;

                case 2:
                    this.<>1__state = -1;
                    UnityEngine.Debug.Log("wait for 3s");
                    this.<>2__current = new WaitForSeconds(3f);
                    this.<>1__state = 3;
                    return true;

                case 3:
                    this.<>1__state = -1;
                    return false;
            }
            return false;
        }

        object IEnumerator.Current
        {
            [DebuggerHidden]
            get
            {
                return this.<>2__current;
            }
        }

        //...省略...
    }
}

有几点可以确定:

  • yield是个语法糖,编译过后的代码看不到yield
  • 编译器在内部创建了一个枚举类 <MyEnumerator>d__1
  • yield return 被声明为枚举时的下一项,即Current属性,通过MoveNext方法来访问结果

OK,通过层层推进,想必你对Untiy中的协程有一定的了解了。再回过头来,我将yield翻译成了中断产出,谈谈我的理解。

  • 中断:传统的方法代码块执行流程是从上到下依次执行,而yield构成的迭代块是告诉编译器如何创建枚举器的行为,反编译得到的结果可以看到,它们的执行并不是连续的,而是通过switch来从一个状态(state)跳转到另一个状态
  • 产出:yield 是和return连用, yield return之后的语句被编译器赋值给current变量,最终通过Current属性产出枚举项

小结

本文的初衷是想介绍如何在Unity中使用多线程,但协程往往是绕不开的话题,于是索性就剖析了下它,故决定单独成一篇。本章内容对多线程开了个头,我将在下篇文章中说说怎样在Unity中使用和管理多线程。
源代码托管在Github上,点击此了解

88x31.png
本博客为 木宛城主原创,基于 Creative Commons Attribution 2.5 China Mainland License发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名 木宛城主(包含链接)。如您有任何疑问或者授权方面的协商,请给我留言。

本文转自木宛城主博客园博客,原文链接:http://www.cnblogs.com/OceanEyes/p/coroutine_vs_threading.html,如需转载请自行联系原作者
目录
相关文章
|
17天前
|
数据库 Android开发 开发者
构建高效Android应用:Kotlin协程的实践指南
【4月更文挑战第2天】随着移动应用开发的不断进步,开发者们寻求更流畅、高效的用户体验。在Android平台上,Kotlin语言凭借其简洁性和功能性赢得了开发社区的广泛支持。特别是Kotlin协程,作为一种轻量级的并发处理方案,使得异步编程变得更加简单和直观。本文将深入探讨Kotlin协程的核心概念、使用场景以及如何将其应用于Android开发中,以提高应用性能和响应能力。通过实际案例分析,我们将展示协程如何简化复杂任务,优化资源管理,并为最终用户提供更加流畅的体验。
|
26天前
|
调度 数据库 Android开发
构建高效Android应用:Kotlin协程的实践与优化
在Android开发领域,Kotlin以其简洁的语法和平台友好性成为了开发的首选语言。其中,Kotlin协程作为处理异步任务的强大工具,它通过提供轻量级的线程管理机制,使得开发者能够在不阻塞主线程的情况下执行后台任务,从而提升应用性能和用户体验。本文将深入探讨Kotlin协程的核心概念,并通过实例演示如何在实际的Android应用中有效地使用协程进行网络请求、数据库操作以及UI的流畅更新。同时,我们还将讨论协程的调试技巧和常见问题的解决方法,以帮助开发者避免常见的陷阱,构建更加健壮和高效的Android应用。
34 4
|
15天前
|
Java
深入理解Java并发编程:线程池的应用与优化
【4月更文挑战第3天】 在Java并发编程中,线程池是一种重要的资源管理工具,它能有效地控制和管理线程的数量,提高系统性能。本文将深入探讨Java线程池的工作原理、应用场景以及优化策略,帮助读者更好地理解和应用线程池。
|
17天前
|
Java Android开发 开发者
构建高效Android应用:Kotlin协程的实践与优化
在响应式编程范式日益盛行的今天,Kotlin协程作为一种轻量级的线程管理解决方案,为Android开发带来了性能和效率的双重提升。本文旨在探讨Kotlin协程的核心概念、实践方法及其在Android应用中的优化策略,帮助开发者构建更加流畅和高效的应用程序。通过深入分析协程的原理与应用场景,结合实际案例,本文将指导读者如何优雅地解决异步任务处理,避免阻塞UI线程,从而优化用户体验。
|
1天前
|
调度 Python
Python多线程、多进程与协程面试题解析
【4月更文挑战第14天】Python并发编程涉及多线程、多进程和协程。面试中,对这些概念的理解和应用是评估候选人的重要标准。本文介绍了它们的基础知识、常见问题和应对策略。多线程在同一进程中并发执行,多进程通过进程间通信实现并发,协程则使用`asyncio`进行轻量级线程控制。面试常遇到的问题包括并发并行混淆、GIL影响多线程性能、进程间通信不当和协程异步IO理解不清。要掌握并发模型,需明确其适用场景,理解GIL、进程间通信和协程调度机制。
17 0
|
5天前
|
Java API 调度
安卓多线程和并发处理:提高应用效率
【4月更文挑战第13天】本文探讨了安卓应用中多线程和并发处理的优化方法,包括使用Thread、AsyncTask、Loader、IntentService、JobScheduler、WorkManager以及线程池。此外,还介绍了RxJava和Kotlin协程作为异步编程工具。理解并恰当运用这些技术能提升应用效率,避免UI卡顿,确保良好用户体验。随着安卓技术发展,更高级的异步处理工具将助力开发者构建高性能应用。
|
6天前
|
Java
探秘jstack:解决Java应用线程问题的利器
探秘jstack:解决Java应用线程问题的利器
14 1
探秘jstack:解决Java应用线程问题的利器
|
11天前
|
移动开发 API Android开发
构建高效Android应用:探究Kotlin协程的优势与实践
【4月更文挑战第7天】 在移动开发领域,性能优化和应用响应性的提升一直是开发者追求的目标。近年来,Kotlin语言因其简洁性和功能性在Android社区中受到青睐,特别是其对协程(Coroutines)的支持,为编写异步代码和处理并发任务提供了一种更加优雅的解决方案。本文将探讨Kotlin协程在Android开发中的应用,揭示其在提高应用性能和简化代码结构方面的潜在优势,并展示如何在实际项目中实现和优化协程。
|
17天前
|
安全 Java 容器
Java并发编程:实现高效、线程安全的多线程应用
综上所述,Java并发编程需要注意线程安全、可见性、性能等方面的问题。合理使用线程池、同步机制、并发容器等工具,可以实现高效且线程安全的多线程应用。
14 1
|
23天前
|
API 数据处理 调度
Python中的异步编程与协程应用
传统的Python编程在处理IO密集型任务时常常面临效率低下的问题,而异步编程和协程技术的引入为解决这一问题提供了有效的途径。本文将介绍Python中异步编程的基本概念,深入探讨asyncio库的使用以及协程在实际项目中的应用,旨在帮助开发者更好地理解和运用异步编程技术。