15.协同程序

15.MonoBehavior中的重要内容-协同程序


15.1 知识点

Unity是否支持多线程?

  1. Unity是支持多线程的,只是新开线程无法访问Unity相关对象的内容。例如this.transform。
  2. 注意:Unity中的多线程 要记住关闭 不然的话会和Unity这个编辑器共生 就算停止运行也会继续执行新线程内容 可以在OnDestroy执行关闭线程逻辑
  3. 在Unity中,不会开启多线程访问Unity相关。当射涉及复杂逻辑的计算的时候,假如都放到主线程里,可能会造成主线程的卡顿。这时就可以开启多线程用于A星寻路算法,网络收发相关。当算好了结果或者收到了消息过后,放在公共的内存区域。主线程判断判断公共的内存区域有没有想要的对象,拿来使用。
//新线程
Thread newThread;

//假设寻路算法会算出来一个点 申明一个变量作为一个公共内存容器 主线程要用就判断是否有内容往里面拿东西
Queue<Vector3> queue = new Queue<Vector3>();

//副线程可能也要拿主线程的数据用来判断做逻辑
Queue<Vector3> queue2 = new Queue<Vector3>();

void Start()
{
    //首先要明确一点
    //Unity是支持多线程的
    //只是新开线程无法访问Unity相关对象的内容
    
    //引用System.Threading命名空间
    newThread = new Thread(newThreadLogic);
    newThread.Start();
    
    //注意:
    //Unity中的多线程 要记住关闭
    //不然的话会和Unity这个编辑器共生 就算停止运行也会继续执行新线程内容
    //可以在OnDestroy执行关闭线程逻辑
}

void Update()
{
    //在主线程判断 副线程有没有往队列里放东西,有的话就拿出来用
    if (queue.Count > 0)
    {
        //取出位置
        this.transform.position = queue.Dequeue();
    }
}

//新线程逻辑
private void newThreadLogic()
{
    while (true)
    {
        Thread.Sleep(1000);
        print("新线程逻辑 每隔一秒的打印");
        
        //我们在Unity中,不会开启多线程访问Unity相关
        //当射涉及复杂逻辑的计算的时候,假如都放到主线程里,可能会造成主线程的卡顿
        //这时就可以开启多线程用于A星寻路算法,网络收发相关
        //当算好了结果或者收到了消息过后,放在公共的内存区域
        //主线程判断判断公共的内存区域有没有想要的对象,拿来使用
        
        //这句代码会报错 不能在副线程控制Unity相关
        //UnityException: get_transform can only be called from the main thread.
        //this.transform.Translate(Vector3.forward * Time.deltaTime);
        
        //相当于模拟 复杂算法 算出了一个结果 然后放入公共容器中
        //用Unity的随机数都不能用 用C#里的随机数
        System.Random r = new System.Random();
        queue.Enqueue(new Vector3(r.Next(-10, 10), r.Next(-10, 10), r.Next(-10, 10)));
        
    }
}

private void OnDestroy()
{
    //关闭线程
    newThread.Abort();
    //关闭线程后置空
    newThread = null;
}

协同程序是什么?

  • 协同程序简称协程
    • 它是“假”的多线程,它不是多线程
    • 它的本质是迭代器

协程的主要作用

  • 将代码分时执行,不卡主线程
  • 简单理解,是把可能会让主线程卡顿的耗时的逻辑分时分步执行

主要使用场景

  • 异步加载文件
  • 异步下载文件
  • 场景异步加载
  • 批量创建时防止卡顿

协同程序和线程的区别

  • 新开一个线程是独立的一个管道,与主线程并行执行。

  • 新开一个协程是在原线程之上开启,进行逻辑分时分步执行。

    • 可以理解为,在主线程开启了一个别的分支,将协程拆分成多部分按逻辑分时分布执行。
    • 在一次主线程循环中,可能只执行了协程的某一部分。
    • 根据返回的逻辑,决定下次执行时机。
    • 未执行时,协程处于挂起状态,等待下次执行时继续执行后续部分。
    • 直到协程的每一部分都执行完毕,这个协程才算执行完毕。
  • 协同程序和线程的区别通俗解释:

    • 多线程如同两根管道。
    • 协程是在线程上开启的一个分支,进行分时分步的执行。

协程的使用

  • 继承MonoBehavior的类 都可以开启 协程函数

声明协程函数

//申明协程函数
//关键点一: 协同程序(协程)函数 返回值 必须是 IEnumerator或者继承它的类型 
IEnumerator MyCoroutine(int i, string str)
{
    //关键点二: 协程函数当中 必须使用 yield return 进行返回
    print("传进来的int值" + i);
    print("yield return 任意数字 或 yield return null");
    print("下一帧后在Update和LateUpdate之间再执行后面的逻辑");
    yield return 123;
    yield return null;

    print("传进来的string值" + str);
    print("yield return new WaitForSeconds(3f)");
    print("等待3秒后在Update和LateUpdate之间再执行后面的逻辑");
    yield return new WaitForSeconds(3f);
}

StartCoroutine方法 开启协程函数

void Start()
{
    //协程函数 是不能够 直接这样去执行的!!!!!!!
    //直接这样调用函数执行没有任何效果
    //MyTimerCoroutine(1, "123");

    //MonoBehaviour中的StartCoroutine方法 开启协程
    //启动协程。
    //常用开启协程的方式
    //注意:StartCoroutine会返回一个Coroutine对象 可以用一个Coroutine变量装起来 可以用于指定协程的关闭
    //用迭代器接口变量接一个协程函数 在丢进StartCoroutine执行
    IEnumerator ie = MyCoroutine(1, "111");
    Coroutine c1 = StartCoroutine(ie);
    //直接把协程函数丢进StartCoroutine执行
    Coroutine c2 = StartCoroutine(MyCoroutine(2, "222"));
    Coroutine c3= StartCoroutine(MyCoroutine(3, "333"));
    Coroutine c4 = StartCoroutine(MyCoroutine(4, "444"));

    //第三步:关闭协程

    //MonoBehaviour中的StopAllCoroutines方法 关闭所有协程
    //停止在该行为上运行的所有协同程序。
    //StopAllCoroutines();

    //MonoBehaviour中的StopCoroutine方法 关闭指定协程
    //停止在该行为上运行的第一个名为 methodName 的协同程序或存储在 routine 中的协同程序。
    //一般都通过关闭返回的协程对象进行关闭 不推荐传入字符串进行关闭
    StopCoroutine(c2);
}

StopCoroutine和StopAllCoroutines方法 关闭协程函数

void Start()
{
    //...

    //MonoBehaviour中的StopAllCoroutines方法 关闭所有协程
    //停止在该行为上运行的所有协同程序。
    //StopAllCoroutines();

    //MonoBehaviour中的StopCoroutine方法 关闭指定协程
    //停止在该行为上运行的第一个名为 methodName 的协同程序或存储在 routine 中的协同程序。
    //一般都通过关闭返回的协程对象进行关闭 不推荐传入字符串进行关闭
    StopCoroutine(c2);
}

yield return 不同内容的含义

IEnumerator MyCoroutine(int i, string str)
{
    //1.下一帧执行
    //yield return 数字;
    //yield return null;
    //在Update和LateUpdate之间执行
    
    //2.等待指定秒后执行
    //yield return new WaitForSeconds(秒);
    //在Update和LateUpdate之间执行
    
    //3.等待下一个固定物理帧更新时执行
    //yield return new WaitForFixedUpdate();
    //在FixedUpdate和碰撞检测相关函数之后执行
    
    //4.等待摄像机和GUI渲染完成后执行
    //yield return new WaitForEndOfFrame();
    //在LateUpdate之后的渲染相关处理完毕后之后
    
    //5.一些特殊类型的对象 比如异步加载相关函数返回的对象
    //之后讲解 异步加载资源 异步加载场景 网络加载时再讲解
    //一般在Update和LateUpdate之间执行
    
    //6.跳出协程
    //yield break;
    
    print("传进来的int值" + i);
    print("yield return 任意数字 或 yield return null");
    print("下一帧后在Update和LateUpdate之间再执行后面的逻辑");
    yield return 123;
    yield return null;
    
    print("传进来的string值" + str);
    print("yield return new WaitForSeconds(3f)");
    print("等待3秒后在Update和LateUpdate之间再执行后面的逻辑");
    yield return new WaitForSeconds(3f);
    
    print("WaitForFixedUpdate()");
    print("等待下一个固定物理帧更新时再执行后面的逻辑");
    yield return new WaitForFixedUpdate();
    
    print("WaitForEndOfFrame()");
    print("等待摄像机和GUI渲染完成后再执行后面的逻辑");
    //主要会用来 截图时 会使用 截图一般是要渲染完再截图
    yield return new WaitForEndOfFrame();
    
    print("跳出协程");
    //注意:直接跳出协程后再StopCoroutine(Coroutine协程对象),会报错,跳出后代表这个协程已经结束了
    yield break;
    
    //协程函数是可以写死循环的
    while (true)
    {
        print("死循环内 yield return new WaitForSeconds(5f);");
        print("死循环内 等待5秒后在Update和LateUpdate之间再执行后面的逻辑");
        yield return new WaitForSeconds(5f);
    }
}

协程受对象和组件失活销毁的影响

  • 协程开启后:
    • 组件或物体销毁,物体失活,则协程不再执行。
    • 组件失活时,协程仍然执行。

总结

  • Unity支持多线程,只是新开线程无法访问主线程中Unity相关内容。一般主要用于进行复杂逻辑运算或者网络消息接收等等。注意:Unity中的多线程一定记住关闭,可以在OnDestroy关闭。
  • 协同程序不是多线程,它是将线程中逻辑进行分时执行,避免卡顿。
  • 继承MonoBehavior的类都可以使用协程。
  • 开启协程方法、关闭协程方法。
  • yield return 返回的内容对于我们的意义。
  • 协程只有当组件单独失活时不受影响,其它情况协程会停止。

15.2 知识点代码

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

public class Lesson15_MonoBehavior中的重要内容_协同程序 : MonoBehaviour
{
    //新线程
    Thread newThread;

    //假设寻路算法会算出来一个点 申明一个变量作为一个公共内存容器 主线程要用就判断是否有内容往里面拿东西
    Queue<Vector3> queue = new Queue<Vector3>();

    //副线程可能也要拿主线程的数据用来判断做逻辑
    Queue<Vector3> queue2 = new Queue<Vector3>();

    void Start()
    {
        #region 知识点一 Unity是否支持多线程?
        //首先要明确一点
        //Unity是支持多线程的
        //只是新开线程无法访问Unity相关对象的内容

        //引用System.Threading命名空间
        newThread = new Thread(newThreadLogic);
        newThread.Start();

        //注意:
        //Unity中的多线程 要记住关闭
        //不然的话会和Unity这个编辑器共生 就算停止运行也会继续执行新线程内容
        //可以在OnDestroy执行关闭线程逻辑

        #endregion

        #region 知识点二 协同程序是什么?
        //协同程序简称协程
        //它是“假”的多线程,它不是多线程
        //他的本质是迭代器

        //它的主要作用
        //将代码分时执行,不卡主线程
        //简单理解,是把可能会让主线程卡顿的耗时的逻辑分时分步执行

        //主要使用场景
        //异步加载文件
        //异步下载文件
        //场景异步加载
        //批量创建时防止卡顿
        #endregion

        #region 知识点三 协同程序和线程的区别

        //新开一个线程是独立的一个管道,和主线程并行执行
        //新开一个协程是在原线程之上开启,进行逻辑分时分步执行
        //可以理解为 在主线程开启了一个别的分支 】
        //把协同程序拿了出来 分成了多部分 按逻辑分时分布的去执行
        //有可能在一次主线程循环中 只执行了某一部分
        //根据返回的逻辑 决定下次执行是什么时候
        //在未执行的时候 协程就处于挂起状态 等待下次执行时继续执行协程后续的部分
        //直到协程函数每一部分都执行完 这个协程才算执行完毕

        //协同程序和线程的区别通俗解释
        //多线程是两根管道
        //协程是线程上开启一个分支进行分时分步的执行

        #endregion

        #region 知识点四 协程的使用

        //继承MonoBehavior的类 都可以开启 协程函数
        //第一步:申明协程函数
        //  协程函数2个关键点
        //  1-1返回值为IEnumerator类型及其子类
        //  1-2函数中通过 yield return 返回值; 进行返回

        //第二步:开启协程函数

        //协程函数 是不能够 直接这样去执行的!!!!!!!
        //直接这样调用函数执行没有任何效果
        //MyTimerCoroutine(1, "123");

        //MonoBehaviour中的StartCoroutine方法 开启协程
        //启动协程。
        //常用开启协程的方式
        //注意:StartCoroutine会返回一个Coroutine对象 可以用一个Coroutine变量装起来 可以用于指定协程的关闭
        //用迭代器接口变量接一个协程函数 在丢进StartCoroutine执行
        IEnumerator ie = MyCoroutine(1, "111");
        Coroutine c1 = StartCoroutine(ie);
        //直接把协程函数丢进StartCoroutine执行
        Coroutine c2 = StartCoroutine(MyCoroutine(2, "222"));
        Coroutine c3= StartCoroutine(MyCoroutine(3, "333"));
        Coroutine c4 = StartCoroutine(MyCoroutine(4, "444"));

        //第三步:关闭协程

        //MonoBehaviour中的StopAllCoroutines方法 关闭所有协程
        //停止在该行为上运行的所有协同程序。
        //StopAllCoroutines();

        //MonoBehaviour中的StopCoroutine方法 关闭指定协程
        //停止在该行为上运行的第一个名为 methodName 的协同程序或存储在 routine 中的协同程序。
        //一般都通过关闭返回的协程对象进行关闭 不推荐传入字符串进行关闭
        StopCoroutine(c2);

        #endregion

        #region 知识点五 yield return 不同内容的含义
        //1.下一帧执行
        //yield return 数字;
        //yield return null;
        //在Update和LateUpdate之间执行

        //2.等待指定秒后执行
        //yield return new WaitForSeconds(秒);
        //在Update和LateUpdate之间执行

        //3.等待下一个固定物理帧更新时执行
        //yield return new WaitForFixedUpdate();
        //在FixedUpdate和碰撞检测相关函数之后执行

        //4.等待摄像机和GUI渲染完成后执行
        //yield return new WaitForEndOfFrame();
        //在LateUpdate之后的渲染相关处理完毕后之后

        //5.一些特殊类型的对象 比如异步加载相关函数返回的对象
        //之后讲解 异步加载资源 异步加载场景 网络加载时再讲解
        //一般在Update和LateUpdate之间执行

        //6.跳出协程
        //yield break;
        #endregion

        #region 知识点六 协程受对象和组件失活销毁的影响
        //协程开启后
        //组件或物体销毁,物体失活 协程不执行
        //组件失活 协程执行
        #endregion

        #region 总结
        //1.Unity支持多线程,只是新开线程无法访问主线程中Unity相关内容
        //  一般主要用于进行复杂逻辑运算或者网络消息接收等等
        //  注意:Unity中的多线程一定记住关闭 可以在OnDestroy关闭
        //2.协同程序不是多线程,它是将线程中逻辑进行分时执行,避免卡顿
        //3.继承MonoBehavior的类都可以使用协程
        //4.开启协程方法、关闭协程方法
        //5.yield return 返回的内容对于我们的意义
        //6.协程只有当组件单独失活时不受影响,其它情况协程会停止
        #endregion
    }

    void Update()
    {
        //在主线程判断 副线程有没有往队列里放东西,有的话就拿出来用
        if (queue.Count > 0)
        {
            //取出位置
            this.transform.position = queue.Dequeue();
        }
    }

    //申明协程函数
    //关键点一: 协同程序(协程)函数 返回值 必须是 IEnumerator或者继承它的类型 
    IEnumerator MyCoroutine(int i, string str)
    {
        //关键点二: 协程函数当中 必须使用 yield return 进行返回

        #region 知识点五 yield return 不同内容的含义
        //1.下一帧执行
        //yield return 数字;
        //yield return null;
        //在Update和LateUpdate之间执行

        //2.等待指定秒后执行
        //yield return new WaitForSeconds(秒);
        //在Update和LateUpdate之间执行

        //3.等待下一个固定物理帧更新时执行
        //yield return new WaitForFixedUpdate();
        //在FixedUpdate和碰撞检测相关函数之后执行

        //4.等待摄像机和GUI渲染完成后执行
        //yield return new WaitForEndOfFrame();
        //在LateUpdate之后的渲染相关处理完毕后之后

        //5.一些特殊类型的对象 比如异步加载相关函数返回的对象
        //之后讲解 异步加载资源 异步加载场景 网络加载时再讲解
        //一般在Update和LateUpdate之间执行

        //6.跳出协程
        //yield break;
        #endregion

        print("传进来的int值" + i);
        print("yield return 任意数字 或 yield return null");
        print("下一帧后在Update和LateUpdate之间再执行后面的逻辑");
        yield return 123;
        yield return null;

        print("传进来的string值" + str);
        print("yield return new WaitForSeconds(3f)");
        print("等待3秒后在Update和LateUpdate之间再执行后面的逻辑");
        yield return new WaitForSeconds(3f);

        print("WaitForFixedUpdate()");
        print("等待下一个固定物理帧更新时再执行后面的逻辑");
        yield return new WaitForFixedUpdate();

        print("WaitForEndOfFrame()");
        print("等待摄像机和GUI渲染完成后再执行后面的逻辑");
        //主要会用来 截图时 会使用 截图一般是要渲染完再截图
        yield return new WaitForEndOfFrame();

        print("跳出协程");
        //注意:直接跳出协程后再StopCoroutine(Coroutine协程对象),会报错,跳出后代表这个协程已经结束了
        yield break;

        //协程函数是可以写死循环的
        while (true)
        {
            print("死循环内 yield return new WaitForSeconds(5f);");
            print("死循环内 等待5秒后在Update和LateUpdate之间再执行后面的逻辑");
            yield return new WaitForSeconds(5f);
        }
    }

    //新线程逻辑
    private void newThreadLogic()
    {
        while (true)
        {
            Thread.Sleep(1000);
            print("新线程逻辑 每隔一秒的打印");

            //我们在Unity中,不会开启多线程访问Unity相关
            //当射涉及复杂逻辑的计算的时候,假如都放到主线程里,可能会造成主线程的卡顿
            //这时就可以开启多线程用于A星寻路算法,网络收发相关
            //当算好了结果或者收到了消息过后,放在公共的内存区域
            //主线程判断判断公共的内存区域有没有想要的对象,拿来使用

            //这句代码会报错 不能在副线程控制Unity相关
            //UnityException: get_transform can only be called from the main thread.
            //this.transform.Translate(Vector3.forward * Time.deltaTime);

            //相当于模拟 复杂算法 算出了一个结果 然后放入公共容器中
            //用Unity的随机数都不能用 用C#里的随机数
            System.Random r = new System.Random();
            queue.Enqueue(new Vector3(r.Next(-10, 10), r.Next(-10, 10), r.Next(-10, 10)));

        }
    }

    private void OnDestroy()
    {
        //关闭线程
        newThread.Abort();
        //关闭线程后置空
        newThread = null;
    }
}

15.3 练习题

利用协程制作一个计秒器

StartCoroutine(MyTimerCoroutine());

// 我的计时器协程
IEnumerator MyTimerCoroutine()
{
    int time = 0;
    // 计时死循环
    while (true)
    {
        print("我的计时器" + time + "秒");
        ++time;
        // 每过一秒再执行
        yield return new WaitForSeconds(1);
    }
}

在场景中创建100000个随机位置的立方体,让其不会明显卡顿

private void Update()
{
    if (Input.GetKeyDown(KeyCode.Space))
    {
        StartCoroutine(CreateCube(100000));
    }
}

// 创建立方体的协程函数
IEnumerator CreateCube(int num)
{
    for (int i = 0; i < num; i++)
    {
        GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
        obj.transform.position = new Vector3(Random.Range(-100, 100), Random.Range(-100, 100), Random.Range(-100, 100));
        // 每创建1000个,就下一帧再创建,这样就没那么卡
        if (i % 1000 == 0)
            yield return null;
    }
}

15.4 练习题代码

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

public class Lesson15_练习题 : MonoBehaviour
{
    void Start()
    {
        #region 练习题一 
        //利用协程制作一个计秒器
        StartCoroutine(MyTimerCoroutine());
        #endregion

        #region 练习题二 
        //请在场景中创建100000个随机位置的立方体,让其不会明显卡顿

        #endregion
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            //直接创建10万个肯定卡
            //for (int i = 0; i < 100000; i++)
            //{
            //    GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
            //    obj.transform.position = new Vector3(Random.Range(-100, 100), Random.Range(-100, 100), Random.Range(-100, 100));
            //}

            StartCoroutine(CreateCube(100000));
        }
    }

    //创建立方体的协程函数
    IEnumerator CreateCube(int num)
    {
        for (int i = 0; i < num; i++)
        {
            GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
            obj.transform.position = new Vector3(Random.Range(-100, 100), Random.Range(-100, 100), Random.Range(-100, 100));
            //每创建1000个 就下一帧再创建 这样就没那么卡
            if (i % 1000 == 0)
                yield return null;
        }
    }

    //我的计时器协程
    IEnumerator MyTimerCoroutine()
    {
        int time = 0;
        //计时死循环
        while (true)
        {
            print("我的计时器"+time + "秒");
            ++time;
            //每过一秒再执行
            yield return new WaitForSeconds(1);
        }
    }
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏