Unity协程和线程的区别深入理解(附实验展示)

article/2025/10/31 23:01:01

Unity协程和线程的区别附实验展示

  • 写在前面
    • 协程、进程、线程的概念
    • 进程与线程的区别
    • 协程与线程的区别
    • 实验1:协程中执行普通函数
    • 实验2:协程中开启另一个协程
    • 实验3:协程中开启WWW请求
    • 实验4:一个脚本中多个协程访问临界资源
    • 实验5:线程中访问临界资源
    • 多线程实验:协程中开启异步操作
    • 实验6:多脚本开协程去访问临界资源
    • (2023/05/24更新)实验7:协程受挂载对象和脚本失落销毁的影响
  • 写在后面

写在前面

前几天被问到线程和协程的区别,发现网上主要是从理论上来讲两者的区别理解起来很抽象,所以我下来做了几个实验来体现线程与协程的区别。
测试代码见:https://github.com/hahahappyboy/UnityProjects/tree/main/%E5%8D%8F%E7%A8%8B%E7%BA%BF%E7%A8%8B%E5%8C%BA%E5%88%AB%E6%B5%8B%E8%AF%95/Assets

协程、进程、线程的概念

进程: 是系统进行资源分配的基本单位,进程是线程的容器。打开unity的程序就是开始了一个进程,每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大。
线程: 是操作系统能够进行资源调度的最小单位(即CPU调度和分派的基本单位),是进程中实际运作的单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的公共资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少。
注意1:属于同一进程的多个线程之间的切换不会引起进程的切换,只有属于不同进程的线程之间的切换才会引起进程的切换。例如QQ间线程的切换(换一个人聊天)不会引起进程的切换,而QQ线程切换为音乐线程会引起进程的切换
注意2:CPU时间片是直接分配给线程的,线程拿到CPU时间片就能执行了,CPU时间片不是先分给进程然后再由进程分给进程下的线程的。所有的进程并行,线程并行都是看起来是并行,其实都是CPU片轮换使用。线程分到了CPU时间片,就可以认为这个线程所属的进程在运行,这样就看起来是进程并行。

协程: 具有多返回点的方法,把时间分片,协程也是运行在Unity主线程里的,是主线程的一部分,Unity只有主线程有些运算不希望放在同一帧里取运算,在同一帧里会卡顿,就可以用协程把他分散不同的帧里运算,这样就可以有一个流畅的体验。将协程代码中由yield return 语句分割的部分分配到每一帧执行。
在这里插入图片描述在这里插入图片描述

进程与线程的区别

进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源
线程是处理器调度的基本单位,但进程不是

协程与线程的区别

协程Coroutine——”伪异步“, 协程不是线程,也不是异步执行的,协程只是把yield return之间的语句分割的部分分配到每一帧执行 (实验1、2)。就和monobehaviour的update函数一样也是在脚本生命周期里执行,属于生命周期的一部分。即unity在每一帧都会处理对象上的协程,也就是说,协程跟update一样都是unity每帧会去处理的函数(update之前,lateupdate之后),所以协程不是异步。除非你在协程里面去开启异步,如网络请求WWW ,才能让协程去创建线程实现异步 (实验3、5)
线程Thread——“真异步 ”, 开启线程后线程和脚本生命周期没有关系了属于两条并行的逻辑,你做你的我做我的。但子线程内不可访问游戏对象或者组件以及相关的Unity API。即unity可以开多线程,但是不能操作unity的api,如调用transform。

在Unity的主线程里只会有一个处于运行状态的协程, 即便不同脚本开启多个协程或则同一脚本开启多给协程也是一个一个顺序执行 (实验4、6),Unity中有个协程调度器,里面有个协程列表协程调度器按照协程列表去执行,从而保证主线程里只能有一个处于运行状态的协程
所以协程之间不会出现临界区资源访问的问题,而但是你开多线程去访问临界资源就会出现问题,因为你没办法控制这个线程会执行到什么时候转换为下一个线程(因为线程是在CPU循环占用的)。 (多线程实验)

实验1:协程中执行普通函数

代码:
在协程里用一个for循环模仿10个耗时操作

public class 协程测试1 : MonoBehaviour
{void Start(){Debug.Log("Start开始");StartCoroutine(协程1());//开启协程Debug.Log("Start结束");}IEnumerator 协程1(){for (int i = 0; i < 10; i++) //循环C{yield return 耗时操作1(); //协程1}}public int 耗时操作1(){Debug.Log("耗时操作开始");for (int i = 0; i < 10000; i++){}Debug.Log("耗时操作结束");return 1;}// 更新数据private int frame = 0;void Update(){frame++;Debug.Log("第"+frame+"帧的"+"Update");}//晚于更新void LateUpdate(){Debug.Log("第"+frame+"帧的"+"LateUpdate");}
}

结果
可以看到,耗时操作执行完成后Unity才会执行接下来的生命周期函数,也就说Unity只是把10个耗时操作通过yield return分在了10帧执行,而不是1帧。但是协程并没有脱离脚本的生命周期,也是在生命周期中每帧执行(Update之后LateUpdate之前)。
在这里插入图片描述

实验2:协程中开启另一个协程

代码: 在协程里再开个协程执行,去执行耗时操作

public class 协程测试2 : MonoBehaviour
{void Start(){Debug.Log("Start开始");StartCoroutine(协程1());//开启协程Debug.Log("Start结束");}IEnumerator 协程1(){Debug.Log("协程开始");for (int i = 0; i < 10; i++) //循环C{yield return StartCoroutine(耗时操作1()); //协程1}Debug.Log("协程开始");}IEnumerator 耗时操作1(){Debug.Log("耗时操作开始");for (int i = 0; i < 10000; i++){}Debug.Log("耗时操作结束");yield return null;}// 更新数据private int frame = 0;void Update(){frame++;Debug.Log("第"+frame+"帧的"+"Update");}//晚于更新void LateUpdate(){Debug.Log("第"+frame+"帧的"+"LateUpdate");}
}

结果
可以看到与实验1并没有什么区别,协程会等开启的协程的yield return执行完成后,才执行下面的生命周期函数。也是每帧的执行,没有脱离生命周期。
在这里插入图片描述

实验3:协程中开启WWW请求

代码: 在协程里开启WWW请求

public class 协程测试3 : MonoBehaviour
{void Start(){Debug.Log("Start开始");StartCoroutine(协程1());//开启协程Debug.Log("Start结束");}IEnumerator 协程1(){Debug.Log("协程开始");for (int i = 0; i < 10; i++) //循环C{Debug.Log("耗时操作开始");UnityWebRequest www = UnityWebRequest.Get("www.baidu.com");yield  return www.SendWebRequest();Debug.Log("耗时操作结束");}Debug.Log("协程结束");}// 更新数据private int frame = 0;void Update(){frame++;Debug.Log("第"+frame+"帧的"+"Update");}//晚于更新void LateUpdate(){Debug.Log("第"+frame+"帧的"+"LateUpdate");}
}

结果
可以看到第1次www操作第1帧开始,第2帧lateupdate后才结束。第2次www操作第2帧lateupdate后开始,第9帧才结束。这才是真正的异步,原因是SendWebRequest函数本身就是真异步操作,是单独开一个线程来向服务器请求数据(多线程模式),从而让该函数脱离了Unity生命周期的循环,只有在该函数执行完后才会回到yield return的地方,继续往下执行,在这期间协程会每帧都检测该函数是否执行完毕。
在这里插入图片描述
在这里插入图片描述

实验4:一个脚本中多个协程访问临界资源

代码: 在线程中访问临界资源

public class 协程测试4 : MonoBehaviour
{private static int n = 5;void Start(){for (int i = 0; i < 10; i++) {StartCoroutine(协程1(i));//开启协程}}IEnumerator 协程1(int id){if (n==5) {n++;Debug.Log("id="+id+" n="+n);}n = 5;yield return null;Debug.Log("id=" + id + "执行完毕");}// 更新数据private int frame = 0;void Update(){frame++;Debug.Log("第"+frame+"帧的"+"Update");}//晚于更新void LateUpdate(){Debug.Log("第"+frame+"帧的"+"LateUpdate");}
}

结果
可以看出即便开多个协程,但在同一个monobehavior脚本里同一时间只会有一个协程在执行,原因很简单因为协程就是在生命周期里执行的,生命周期是顺序执行的,那么开启的协程也是按顺序执行的。
在这里插入图片描述

实验5:线程中访问临界资源

代码: 线程中访问临界资源

public class 线程 {// private static object lockObject = new object();private static int n = 5;public static void Main() {for (int i = 0; i < 10; i++) {new Thread(ThreadMain).Start();}}public static void ThreadMain() {// lock (lockObject) {if (n==5) {n++;Console.WriteLine("n="+n);}n = 5;// }     }
}

结果
可以看到在不加锁的情况下,因为线程是异步的,其中有线程执行n++后还没打印,CPU就换为其他线程执行了n=5,才导致打印了5
在这里插入图片描述

多线程实验:协程中开启异步操作

代码: 协程中开启异步操作

public class 协程测试5 : MonoBehaviour
{void Start(){Debug.Log("Start开始");StartCoroutine(协程1());//开启协程Debug.Log("Start结束");}IEnumerator 协程1(){yield return 耗时操作1(); //协程1}private async Task<int> 耗时操作1(){return await Task.Run( () => {Debug.Log("耗时操作开始");for (int i = 0; i < 10000000; i++) //循环B{}Debug.Log("耗时操作结束");return 1;});}// 更新数据private int frame = 0;void Update(){frame++;Debug.Log("第"+frame+"帧的"+"Update");}//晚于更新void LateUpdate(){Debug.Log("第"+frame+"帧的"+"LateUpdate");}
}

结果
可以看到在协程中开启异步后,过了两帧才执行完,而不想在协程中开启协程那种每帧去执行。因为异步是开线程执行,此时已经脱离了主线程。
在这里插入图片描述

实验6:多脚本开协程去访问临界资源

代码: 多脚本开协程去访问临界资源

public static class  临界资源 {public static int n = 5;public static void ThreadMain() {if (n==5) {n++;Debug.Log("n="+n);}n = 5;}
}public class 协程测试6 : MonoBehaviour
{void Start(){StartCoroutine(协程1());//开启协程}IEnumerator 协程1(){yield return 抢占支援(); //协程1}public int 抢占支援(){临界资源.ThreadMain();return 1;}}

结果
即便开了100个脚本,全部打印的n=6。原因是在unity的主线程里只会有一个处于运行状态的协程,即便不同脚本开启多个协程或则同一脚本开启多给协程也是一个一个顺序执行。这是因为unity中有个协程调度器,里面有个协程列表协程调度器按照协程列表去执行,从而保证主线程里只能有一个处于运行状态的协程。
在这里插入图片描述

(2023/05/24更新)实验7:协程受挂载对象和脚本失落销毁的影响

代码:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class CoroutineExample : MonoBehaviour
{private Coroutine myCoroutine;private void Start(){// 启动协程myCoroutine = StartCoroutine(MyCoroutine());}private void Update() {Debug.Log("Update执行");}private void LateUpdate() {Debug.Log("LateUpdate执行");}private IEnumerator MyCoroutine(){Debug.Log("Coroutine开始");// 执行一段时间的协程逻辑yield return new WaitForSeconds(4f);Debug.Log("Coroutine执行4s");// 继续执行协程yield return new WaitForSeconds(4f);Debug.Log("Coroutine又执行4秒");// 继续执行协程yield return new WaitForSeconds(2f);Debug.Log("Coroutine又又执行2秒");}private void OnDisable(){// 当脚本禁用时停止协程的执行// StopCoroutine(myCoroutine);// Debug.Log("Coroutine停止,因为物体失活");}private void OnDestroy(){Debug.Log("脚本或物体被销毁");}
}

结果
1.当挂载对象和脚本销毁后,协程不执行。
2.当挂载对象失活时,协程不执行,激活后也不会执行。这是为了逻辑一致性,
对象的激活状态通常与其在游戏中的逻辑一致性相关。当对象失活时,通常表示该对象
在当前场景中不处于活动状态,对象其他行为也停止,通过暂停协程的执行,确保了对象的行为与其激活状态保持一致,不会出现逻辑错误。
3.当只有脚本失活时,协程继续执行(一般在OnDisable0中停止所有需要持续执行的协程)。这是为了保留状态和数据,协程可以在脚本重新激活后继续执行,并目保留之前的状态和数据。这对于需要在中断或暂停期间保存和恢复状态的情况非常有用。例如,在游戏中的对话系统中,可以使用协程来显示对话框,允许玩家暂停或关闭对话框,然后在需要时继续显示对话框,而不会丢失之前的对话进度。

写在后面

哎,Unity工作好难找啊,55555


http://chatgpt.dhexx.cn/article/n6pbqwBl.shtml

相关文章

Unity 协程 Unity Task UniTask

协程 使用 StartCoroutine 和 IEnumerator yield return null 暂停执行并随后在下一帧恢复 yield return new WaitForSeconds(1f); 延迟1秒 waitfor系列有好几个 WaitForSeconds 和 WaitForSecondsRealtime 的区别 使用缩放时间将协程执行暂停指定的秒数。 实际暂停时间等于…

Unity 协程探究

一、官方手册中的描述 1、Manual/Coroutines 函数在调用时, “从调用到返回” 都发生在一帧之内&#xff0c;想要处理 “随时间推移进行的事务”&#xff0c; 相比Update&#xff0c;使用协程来执行此类任务会更方便。 协程在创建时&#xff0c;通常是一个 “返回值类型 为 …

Unity 协程底层原理解析

1、协程 unity是单线程设计的游戏引擎&#xff0c;unity实际上有多条渲染线程&#xff0c;但对于unity调用我们编写的游戏脚本&#xff0c;都是放在一个主线程当中进行调度的。因此对于我们写的游戏脚本unity是单线程的。 协程不是进程或者线程&#xff0c;它的执行过程更类似…

Unity协程那些事儿

Unity协程那些事儿 1、什么是协程&#xff1f;2、协程的使用3、关于yield4、关于IEnumerator/IEnumerable5、从IEnumerator/IEnumerable到yield6、Unity协程机制的实现原理7、源码分析8、总结 1、什么是协程&#xff1f; 用过Unity的应该都知道协程&#xff0c;今天就给大家来…

Unity⭐️当Unity协程遇到while

文章目录 Unity协程是一个老生常谈的知识点了&#xff0c;但今天博主却差点被一篇文章忽悠了 那就是这句话&#xff1a; 为什么说这句话不对呢 那就是当遇到while时&#xff0c;我们再来检验这句话的正确性&#xff1a; 按上面那句话&#xff0c;应该每次yield return后&…

深入探讨Unity协程及其使用

深入探讨Unity协程及其使用 协程 协程在Unity中是个很重要的东东&#xff0c;相信很多人都使用过&#xff0c;它能够非常方便的进行异步等待操作&#xff0c;可以说&#xff0c;用好协程&#xff0c;可以使你的代码更加优雅&#xff0c;然而&#xff0c;如果用不好&#xff0…

Unity 协程、模拟协程

理解协程 IEnumeratoryield 强大的迭代器 IEnumerator 就是一个函数容器 里面保存了一个一个的函数 IEnumator会依次执行每个函数 而每个函数都有一个返回值 保存在IEnumator.Currect里面 看下面这个例子 IEnuermator start_Coroutine(){Debug.Log("HellWord")…

unity 协程特点

协程 一. Unity中使用协程1. 什么是协程2. 如何使用3. 协程的应用场景创建补间动画打字机效果异步加载资源 4. 注意事项 二. Unity协程的底层原理1. 协程本体&#xff1a;C#的迭代器函数2. 协程调度&#xff1a;MonoBehaviour生命周期中实现3. Unity协程的架构 三. 扩展Unity的…

Unity协程深入及实现

Unity协程深入 文章目录 Unity协程深入Unity协程简介Unity协程的用法协程的原理协程的缺陷自己实现协程参考 Unity协程简介 Unity中的协程能够允许我们将一个任务分散到多个帧中。注意&#xff1a;协程&#xff08;协同执行&#xff09;和多线程有本质区别。 协程不是线程&…

unity 协程

首先声明&#xff1a;协程不是线程&#xff0c;协程在主线程中运行&#xff0c;而线程是单独开辟线程 以下是u3d关于协程的调用机制的解释&#xff1a; “在Unity3D中&#xff0c;使用MonoBehaviour.StartCoroutine方法即可开启一个协同程序&#xff0c;也就是说该方法必须在…

【Unity】Unity协程(Coroutine)的原理与应用

文章目录 前言一、什么是协程二、应用场景1.异步加载资源2.将一个复杂程序分帧执行3.定时器 三、协程的使用注意事项 四、Unity协程的底层原理1. 协程本体&#xff1a;C#的迭代器函数2. 协程调度&#xff1a;MonoBehaviour生命周期中实现 五、参考资料 前言 本文是作者在学习U…

Unity-协程详解

1. 简介 unity的**协程&#xff08;Coroutine&#xff09;**只是在c#的基础上做了一层封装&#xff0c;其实yield是C#的关键字。 unity协程是一个能够暂停协程执行&#xff0c;暂停后立即返回主函数&#xff0c;执行主函数剩余的部分&#xff0c;直到中断指令完成后&#xff…

Unity 协程(Coroutine)原理与用法详解

前言&#xff1a; 协程在Unity中是一个很重要的概念&#xff0c;我们知道&#xff0c;在使用Unity进行游戏开发时&#xff0c;一般&#xff08;注意是一般&#xff09;不考虑多线程&#xff0c;那么如何处理一些在主任务之外的需求呢&#xff0c;Unity给我们提供了协程这种方式…

linux的用户管理

1.linux的用户管理 linux的用户管理和组管理&#xff0c; 每个用户都必须要有一个且仅有一个初始组&#xff0c;可以有多个附加组&#xff0c;使用useradd命令创建用户时&#xff0c;如果没有指定初始组&#xff0c;系统默认会创建一个于其同名的组。 用户和组信息保存在4个文…

Linux用户管理练习

Linux下用户分为3类&#xff1a;超级用户&#xff08;root&#xff09;、系统用户、普通用户。 超级用户的用户名为root&#xff0c;它具有一切操作权力&#xff0c;因此为安全起见&#xff0c;建议不要轻易的在root账户下面对文件进行操作。在Linux操作系统的字符界面&#xf…

Linux用户管理工具

Linux用户管理工具 1. 用户 1.1创建用户 useradd -m username该命令为用户创建相应的帐号和用户目录/home/username&#xff1b; passwd username该命令为用户设置密码 1.2 删除用户 userdel -r username不带选项使用 userdel&#xff0c;只会删除用户。用户的家目录将…

Linux上的用户管理

Linux是一个多用户多任务的系统&#xff0c;任何人想要访问系统资源&#xff0c;必须通过登录账号来访问系统资源 添加用户 useradd&#xff1a;添加用户的命令&#xff08;root才能添加用户&#xff09; 用法&#xff1a;useradd 用户名 例&#xff1a;useradd water 就创建…

实现Linux用户管理

1.添加用户组&#xff1a;groupadd 用户组名称 2.创建用户 &#xff08;一&#xff09;仅创建用户&#xff1a;useradd 用户名 &#xff08;二&#xff09;指定用户所属的用户组&#xff1a; useradd -g 用户组 3.设置用户密码 &#xff08;一&#xff09;passwd 普通用户名 …

linux用户管理及操作指令

1、首先了解下linux是一个多用户多任务的操作系统。任何一个需要使用资源的用户都需要从linux系统中分配一个用户角色&#xff0c;比如&#xff1a;root、user、、然后使用对应账号进入系统。一个root用户下面能创建多个用户&#xff0c;每个用户下面对应一个目录 2、添加用户…