自己写笔记的过程相对看视频和读文章的认识更稳固,效率是极佳的。

进程、线程、协程的关系:

线程和协程都是进程的子集,一个进程可以有多个线程,一个线程可以有多个协程,进程基于程序主体。

IO密集型一般使用多线程或多进程。CPU密集型一般使用多进程。强调非阻塞异步并发的一般都用协程。

进程:

        进程是系统分配资源和调度资源的一个独立单位,每个进程都有自己的独立内存空间,不同进程间可以进行进程间通信,进程重量级比较大,占据独立内存,上下文进程间的切换开销 比较大,但先对稳定安全。进程的上级为操作系统,有自己固定的堆栈。

线程:

        线程也被称为轻量级进程,是操作系统调度(CPU调度)执行的最小单位,是进程的子集。

        线程本身基本不拥有资源,而是访问隶属于进程的资源,一个进程拥有至少一个或者多个线程,线程间共享进程的地址空间。

        由于线程时阻塞式的,如果想要同步执行IO,每个IO都必须开启一个新线程,多线程开销较大,适合多任务处理,进程崩溃不影响其他进程,而线程知识一个进程的不同执行路线。

        线程有自己的堆栈,却没有单独的地址空间,进程死就等于所有线程死,所以多进程要比多进程健壮。但在进程切换是,消耗资源较大,效率较差。

        线程时并发的,是阻塞式同步的,一但资源锁死,线程将陷入混乱。在同步线程的执行过程中。线程的执行切换是由CPU轮转时间片的分配来决定的。

协程:

        协程是比线程更轻量级的存在,协程不由操作系统内核所管理,而是完全由程序所控制(也就是在用户状态执行)。

        协程的好处是性能大幅提升,不会像线程切换那样消耗资源。同一时间只能执行某个协程,开辟多个协程开销不大。适合对任务进行分时处理。

        协程有自己的寄存器和上下文栈。协程调度切换时,将寄存器和上下文保存到其他地方,并在协程切换回来时回复之前保存的寄存器和上下文栈。由于直接对栈进行操作,本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文切换非常快。

        一个线程可以有多个协程,一个进程也可以单独拥有多个协程,线程和进程都是同步机制,而协程是异步机制,无需阻塞。协程能保留上一次调用时的状态,每次过程重入是,就相当于进入上一次调用状态。多协程间对CPU的使用是依次进行的,每次只有一个协程工作,其他协程处于休眠状态。

        实际上多个协程是在一个线程中,只不过每个协程对CPU进行分时。协程可以访问和使用Unity的所有方法和Component。函数(子程序)的调用是通过栈实现的,一个线程就是执行一个函数,函数调用总是一个入口,一个返回,调用顺序是明确的,而协程在函数内部是可以中断的,然后转而执行其他函数,在合适的时候再返回来继续执行。函数(子程序)的切换不是由线程切换,而是程序自身控制,因此没有线程切换开销。和多线程相比,线程越多,协程的性能优势就越明显,切协程因为依次执行,不存在线程安全问题,变量访问不会冲突,共享资源也无序枷锁,只需要判断状态即可。

协程的语法:

        yield :暂停,通常用 yield return null 来暂停协程、

        StartCoroutine(方法名()) :恢复执行。

        WaitForSeconds :引入时间延迟,默认情况下,协程将在yield后的帧上恢复。使用yield return new WaitForSecond(1f)后 延迟1秒后执行协程

协程的内核:

        从程序的角度来讲,协程的核心就是迭代器。想要定义一个协程方法有两个因素,第一:方法的返回值为IEnumerator。第二,方法中有yield关键词。当代码满足以上两个条件是,此方法的执行就具有了迭代的特质,其核心就是MoveNext方法。方法内的内容会被分为两个部分:yield之前的代码和yield之后的代码。yield之前的代码会在第一次执行MoveNext时执行,yield之后的代码会在第二次执行MoveNext方法时执行,而在Unity中,MoveNext的执行时机是以帧为单位的,无论是你设置了延迟时间,还是通过按钮调用MoveNext,亦或是根本没有设置执行条件,Unity都会在每一帧的生命周期中判断当前帧是否满足当前协程所定义的条件,一旦满足,当前帧就会抽出CPU时间执行你所定义的协程迭代器的MoveNext。注意,只要方法中有yield语句,那么方法的返回值就必须时IEnumerator,不然无法通过编译。

  1. 协程方法的调用看的是迭代器,必须要获取到这个迭代器,然后将索引移动到下一步(也就是执行MoveNext方法)才会真的开始执行方法内容。
  2. 代码的停止是以yield为节点的,看到yield就停,满足yield条件就继续执行,直到看不到yield为止。

        不用手动调用MoveNext方法,而是使用StartCoroutine方法。当我们调用了这个StartCoroutine方法,就等于把这个迭代器Iterator的MoveNext执行交个了程序端。程序会逐帧扫描上次yield指定的条件是否已经满足,如果满足,就继续执行下一次。yield暂停后按下一次执行的时间并不是准确的时间节点执行,而是在某一帧的生命周期中判断当前协程是否达到了执行时机(及满足yield执行条件),如果不满足就继续执行下一帧,如果满足就继续执行yield后面的内容。

        一帧执行一次用处不大,无法根据项目需求实现我们项目要实现的效果,这时我们可以用yield的条件,通过这个条件可以控制代码执行的时机,达到项目需求。

        例如 yield return new WaitForSeconds(1);这里的表示会延迟1S,当然不止有这一种延迟功能,也有一些延迟N帧、下一帧结束等方法也叫执行条件,这些执行条件的判断节点是在Update之后,LateUpdate之前。Unity的生命周期里有一个顺序解释,这个要着重记下,在项目中注意这些细节会让你实现的效果更准确,也就是说当代码中指定的条件是WaitForSconds判断点是在Update之后LateUpdate之前,就是说,如果代码中的指定的条件是WaitForSeconds当Update执行结束就会判断yield指定的条件是否已经满足了,如果满足,就会在此时完成这个协程的下一步操作。

         StartCoroutine是否只做了MoveNext的事情呢? 实际上StartCoroutine并不只是用于将迭代器的指针移动到下一步,StartCoroutine方法的作用是管理了一次协程的全部过程,它包含了开启协程,移动指针,自然结束协程、初始化协程等一系列操作,也就是说,当我们调用一次StartCoroutine方法,一次协程就被开启了一次。

        与之对应,有Start就有Stop,Unity在MonoBehaviour中为我们提供了多个协程方法,以便我们调用,例如StopCoroutine、StopAllCoroutines等。

        Unity 的协程是作用在游戏对象上的,协程开启后,简单的禁用脚本组件是不会停止协程的,只有当前的物体本身被禁用才会终值协程。

        另外,由于协程的启动,会有一定的内存消耗,而yield不会有后续消耗,所以尽量不要频繁的调用StartCoroutine方法来开启协程。当然,与进程、线程比较式,协程的消耗无疑是最小的,所以如果需要用到异步的操作,可以尽情使用协程。

yield return 对象:

上面说到有很多延迟的方法通过yield return ???? 来完成效果的

1 null或数字:在Update后执行,适合分解好事的逻辑处理。
        2 WaitForFixedUpdate : 在FixedUpdate后执行,适合分解物理操作。
        3 WaitForSeconds: 在指定时间后执行,适合延迟调用。
        4 WaitForScondsRealtime:在指定时间后执行,适合延迟调用,不受时间缩放影响。
        5 WaitForEndOfFrame:在每帧结束后执行,适合相机跟随操作。
        6 Coroutine :在另一个协程执行完毕后再执行。
        7 WaitUnitl: 在委托返回True是执行,适合等待某一操作
        8 WaitWhile: 在委托返回False是执行,适合等待某一操作     
        9 WWW: 在请求结束后执行,适合加载数据,文件 贴图 材质等。
        10 UnityWebRequest 和WWW一样 请求结束后执行,适合加载数据,文件 贴图 材质等。

        yield return Coroutine的机制是等待指定的协程完全结束后才继续执行的,而不是与指定协程进行穿插执行。这一点一定要明确。

协程案例:

        项目中通常有两个作用: 延时调用 分解操作

案例1
当玩家按下某个按键后触发一个渐变功能,此功能并不需要每一帧都渐变,这时就可以使用协程,按照一定的时间间隔调用。大致代码如下:

IEnumerator FadeOut()
{
	Color c = renderer.material.color;
	do
	{
		c.a -= 0.02f;  // 改变颜色
		yield return new WaitForSeconds(0.2f); // 延迟0.2秒执行
	} while (c.a > 0);
	if (c.a < 0)
	{
		c.a = 0;
	}
}
void Update()
{
	if (Input.GetKeyDown("f"))
	{
		StartCoroutine(FadeOut());
	}
}

案例2
用协程嵌套实现寻路功能,这样做的好处有:1、让Update不再臃肿;2、让逐帧操作变成了单次调用,增加代码可读性。
将类似的需要逐帧或跳帧操作的功能都用协程封装成工具类,代码的可读性就会大大增强,且运行效率也有所提升。

/// <summary>
/// 嵌套协程实现寻路
/// </summary>
public class PathFinding : MonoBehaviour
{
    public Transform[] wayPoints;

    public float moveSpeed;

    public IEnumerator FindPath(Transform[] wayPoints)
    {
        for (int i = 0; i < wayPoints.Length; i++)
        {
            yield return StartCoroutine(MoveToTarget(wayPoints[i].position));
        }
    }

    private IEnumerator MoveToTarget(Vector3 position)
    {
        transform.LookAt(position);
        while (Vector3.Distance(transform.position, position) > 0.1f)
        {
            transform.position = Vector3.MoveTowards(transform.position, position, moveSpeed);
            yield return new WaitForFixedUpdate();
        }
    }

    private void OnGUI()
    {
        if (GUILayout.Button("走你"))
        {
            StartCoroutine(FindPath(wayPoints));
        }
    }
}

案例3
给每个敌人加一个警报检测,这种功能可以放在Update中执行,但每一帧执行没有必要,这时就可以使用协程,将此功能从Update中剥离出来。

案例4
当程序需要异步加载资源或者获取网络资源时,可以使用WWW或UnityWebRequest协程。

案例5
创建补间动画。

案例6
打字机效果。

案例7
定时器操作。

协程总结:

        在Unity的生命周期中,有很多步骤都涉及到协程,我们可以通过协程来实现在生命周期的不同步骤下执行任务,协程是依赖于迭代器原理执行的,其本身并不能加快程序运行速度,但其功能却能实现前后台的异步、定时操作任务等,且其代码规格从Update中分离出来,即简化了Update,又增加了功能代码的可读性。大型游戏甚至可以做一套协程管理器来实现功能的管理,以及代码的审核,让代码更具有可维护性。

注意:启动一个协程会消耗少量的内存,在方法调用时却不会有后续消耗。如果内存消耗和垃圾回收是严重的问题,应该长诗避免产生太短时间的协程,并避免在运行时调用太多StartCoroutine().