当前位置: 首页 > news >正文

【一文了解】Unity的协程(Coroutine)与线程(Thread)

目录

协程(Coroutine)

1.协程是什么?

2.协程的核心特点

3.适合场景

4.协程的核心内容

5.示例

6.协程使用注意事项

线程(Thread)

1.基础概念

2.示例

3.线程使用注意事项

协程 VS 线程


       本篇文章来分享一下Unity协程与线程。协程和线程都是实现异步操作和多任务处理的重要概念,但它们有着不同的工作机制和适用场景。

协程(Coroutine)

1.协程是什么?

       协程是Unity中一种特殊的“分步执行”程序组件,和普通方法(函数)类似,但更灵活

普通方法:从开头执行到结尾,只有一个入口和一个出口。

协程:可以有多个入口和出口,能在执行过程中“暂停”(用yieldreturn),之后从暂停处继续执行

简单说,协程能把一个“需要多帧才能完成的任务”拆分成多步,每帧执行一步,既完成了任务,又不会让游戏卡顿。

2.协程的核心特点

(1)单线程:协程和普通代码运行在同一线程中,不是多线程,所以不用担心线程安全问题(如变量冲突)。

(2)分步执行:通过yield return控制执行节奏,比如“等1秒再执行下一步”“等某帧结束再执行”。

3.适合场景

(1)多帧完成的任务(如A*寻路、复杂动画)。

(2)延迟执行(如3秒后播放技能特效)。

(3)异步操作(如网络请求、资源加载)。

4.协程的核心内容

(1)IEnumerator接口:协程方法必须返回IEnumerator类型,提供了“迭代执行”的能力。

(2)yield return关键字:指定协程“暂停后,下一次从哪继续执行”。常见的yield return类型:

null或0:下一帧Update后继续执行。

new WaitForSeconds(时间):等待指定秒数后,下一帧Update后继续执行。

new WaitForFixedUpdate():等待到下一次FixedUpdate(物理更新帧)后执行。

new WaitForEndOfFrame():等待到当前帧所有渲染完成后执行。

WWW或UnityWebRequest:等待网络请求完成后执行(适合下载资源)。

(3)协程的启动

StartCoroutine(协程方法名()):直接调用协程方法。

StartCoroutine("协程方法名"):通过字符串指定协程方法名(需注意,这种方式停止时要对应使用字符串形式)。

(4)协程的停止

StopCoroutine("协程方法名"):停止“通过字符串启动”的协程。

StopAllCoroutines():停止当前脚本中所有正在运行的协程。

5.示例

       实现协程的“延迟执行”、“循环执行”、“停止协程”

using System.Collections;
using UnityEngine;public class CoroutineDemo : MonoBehaviour
{//用于存储协程的“引用”,方便后续停止它private Coroutine myCoroutine;private void Start(){//启动“延迟打印”协程StartCoroutine(DelayPrint());//启动“循环打印”协程,并保存引用myCoroutine = StartCoroutine(LoopPrint());//5 秒后停止循环打印协程StartCoroutine(StopLoopAfter5Seconds());}/// <summary>/// 延迟 2 秒后打印/// </summary>/// <returns></returns>private IEnumerator DelayPrint(){Debug.Log("协程 DelayPrint:开始执行,准备等待 2 秒");//暂停协程,等待 2 秒yield return new WaitForSeconds(2);Debug.Log("协程 DelayPrint:等待 2 秒结束");}/// <summary>/// 每 1 秒打印一次,直到被停止/// </summary>/// <returns></returns>private IEnumerator LoopPrint(){int count = 1;while (true) //无限循环,直到被外部停止{Debug.Log($"协程 LoopPrint:第 {count} 次打印");count++;//暂停协程,等待 1 秒yield return new WaitForSeconds(1);}}/// <summary>/// 5 秒后停止“循环打印”协程/// </summary>/// <returns></returns>private IEnumerator StopLoopAfter5Seconds(){Debug.Log("协程 StopLoopAfter5Seconds:准备等待 5 秒");//暂停协程,等待 5 秒yield return new WaitForSeconds(5);Debug.Log("协程 StopLoopAfter5Seconds:等待 5 秒结束,停止循环打印协程");//停止指定的协程(通过之前保存的 myCoroutine 引用)StopCoroutine(myCoroutine);}private void Update(){//按空格键,停止当前脚本所有协程if (Input.GetKeyDown(KeyCode.Space)){Debug.Log("Update:用户按空格键,停止所有协程");StopAllCoroutines();}}
}

效果

例子中协程的执行顺序:

(1)协程启动顺序:Start() 中启动的多个协程,几乎同时进入 “初始执行阶段”(即执行到第一个 yield return 前的代码)。

(2)后续执行顺序:由yield return的“恢复条件”决定(如WaitForSeconds(2) 比WaitForSeconds(5) 更早恢复)。

(3)停止逻辑:StopCoroutine(myCoroutine) 会立即终止指定协程(协程 B 在 5 秒后被停止);StopAllCoroutines() 会终止脚本中所有正在运行的协程(用户按空格时触发)。

6.协程使用注意事项

(1)停止限制:StopCoroutine("方法名")只能停止“用字符串启动”的协程;如果用StartCoroutine(方法名())启动,需用StopCoroutine(协程引用)停止(如示例中用myCoroutine引用)。

(2)单线程:协程和普通代码在同一线程,若协程中有“非常耗时的计算”(如循环10万次),仍会导致游戏卡顿。这种情况可以拆分成多帧执行(每帧执行一小部分)。

(3)参数限制:协程方法(返回IEnumerator的方法)不能有ref或out类型的参数。

(4)调试困难:Unity没有直接的工具显示“当前有哪些协程在运行”,所以建议自己做好协程的管理(如保存关键协程的引用)。

线程(Thread)

1.基础概念

(1)定义:线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件句柄等。

(2)工作原理:操作系统为每个线程分配一定的执行时间片,通过快速地在不同线程之间切换,使得每个线程看起来像是在同时运行(实际上在单核CPU中,同一时刻只有一个线程在执行,但由于切换速度极快,给人以并发执行的错觉;在多核CPU中,不同线程可以真正并行执行)。

(3)多线程:多线程是指在一个程序中同时运行多个线程,每个线程可以执行不同的任务,从而实现并发处理

多线程优势:

(1)提高性能:在多核CPU环境下,多线程可以充分利用CPU的多个核心,将不同的任务分配到不同核心上并行执行,从而加速程序的运行。比如,在进行图像渲染和音频处理的游戏中,一个线程负责渲染画面,另一个线程负责音频播放,二者可以同时进行,提高整体效率。

(2)改善响应性:对于一些耗时较长的任务,如网络请求、文件读写等,如果在主线程中执行,可能会导致程序界面卡顿。使用多线程可以将这些任务放到后台线程执行,主线程则可以继续处理用户交互,保证界面的流畅响应。

多线程缺点:

(1)复杂性增加:多线程编程需要考虑线程同步、资源共享等问题,否则容易出现数据竞争、死锁等错误。例如,当多个线程同时访问和修改同一个共享变量时,可能会导致数据不一致。

(2)调试困难:由于线程的执行顺序具有不确定性,很难预测线程在何时执行,这使得多线程程序的调试变得更加困难。

(3)资源消耗:线程的创建和销毁需要消耗一定的系统资源,过多的线程可能会导致系统性能下降。

2.示例

       展示文本的读取进度以及文本内容。

功能说明

主线程:Unity主线程负责UI显示(如进度条、结果文本),避免UI卡顿。

子线程:在后台读取3个文本文件,读取完成后通过MainThreadDispatcher(主线程调度器)将结果更新到UI(Unity 不允许子线程直接操作UI)。

线程安全:使用lock确保数据共享安全,使用主线程调度器避免UI操作异常。

using UnityEngine;
using System.Collections.Generic;/// <summary>
/// 主线程调度器:用于子线程向主线程投递任务(如更新 UI)
/// </summary>
public class MainThreadDispatcher : MonoBehaviour
{//单例实例private static MainThreadDispatcher instance;//主线程任务队列private readonly Queue<System.Action> mainThreadTasks = new Queue<System.Action>();//确保场景中只有一个调度器private void Awake(){if (instance == null){instance = this;}else{Destroy(gameObject);}}//主线程每帧执行任务队列private void Update(){lock (mainThreadTasks)//线程安全地访问任务队列{while (mainThreadTasks.Count > 0){//执行主线程任务(如更新 UI)mainThreadTasks.Dequeue()?.Invoke();}}}/// <summary>/// 向主线程投递任务/// </summary>/// <param name="task">要在主线程执行的任务(如 UI 操作)</param>public static void EnqueueTask(System.Action task){if (instance == null){Debug.LogError("MainThreadDispatcher 未初始化!请在场景中添加该脚本");return;}lock (instance.mainThreadTasks){instance.mainThreadTasks.Enqueue(task);}}
}
using UnityEngine;
using UnityEngine.UI;
using System.IO;
using System.Threading;
using TMPro;
using System;public class FileReaderThread : MonoBehaviour
{[Header("UI 组件")]public TextMeshProUGUI statusText;//状态文本public TextMeshProUGUI resultText;//结果文本,显示文件内容public Slider progressSlider;//进度条,显示读取进度//线程安全锁对象private readonly object progressLock = new object();//已完成的任务数private int completedTasks = 0;//总任务数private const int TotalTasks = 3;private void Start(){//初始化 UIprogressSlider.maxValue = TotalTasks;progressSlider.value = 0;//确保主线程调度器已存在if (FindObjectOfType<MainThreadDispatcher>() == null){new GameObject("MainThreadDispatcher").AddComponent<MainThreadDispatcher>();}//启动多线程读取文件StartFileReadingThreads();}/// <summary>/// 启动文件读取线程/// </summary>private void StartFileReadingThreads(){statusText.text = "正在后台读取文件...";//定义要读取的文件路径(需放在StreamingAssets路径下)string[] filePaths = new string[]{Path.Combine(Application.streamingAssetsPath, "test1.txt"),Path.Combine(Application.streamingAssetsPath, "test2.txt"),Path.Combine(Application.streamingAssetsPath, "test3.txt")};//为每个文件创建线程foreach (var filePath in filePaths){string currentFilePath = filePath;//避免闭包问题new Thread(() => ReadFileInThread(currentFilePath)).Start();}}/// <summary>/// 子线程:读取文件内容/// </summary>private void ReadFileInThread(string filePath){try{//模拟耗时操作(如网络请求文件)Thread.Sleep(1000);//读取文件内容string content = File.ReadAllText(filePath);//任务完成:更新进度(线程安全)lock (progressLock){completedTasks++;}//1.向主线程投递“更新进度条”任务MainThreadDispatcher.EnqueueTask(() =>{progressSlider.value = completedTasks;statusText.text = $"已完成 {completedTasks}/{TotalTasks} 个文件读取";});//2.向主线程投递“更新结果文本”任务MainThreadDispatcher.EnqueueTask(() =>{resultText.text += $"文件:{Path.GetFileName(filePath)}\n内容:{content}\n\n";//所有任务完成后提示if (completedTasks == TotalTasks){statusText.text = "所有文件读取完成!";}});}catch (Exception ex){//向主线程投递“显示错误”任务MainThreadDispatcher.EnqueueTask(() =>{resultText.text += $"读取文件失败:{ex.Message}\n\n";});}}
}

场景设置

效果

3.线程使用注意事项

       Unity本身是单线程模型,主线程负责渲染、更新GameObject状态、处理用户输入等关键任务。在Unity中使用多线程需要借助.NET提供的System.Threading命名空间下的相关类(如Thread、ThreadPool等)。在Unity中使用线程需要特别注意以下几点:

(1)不能直接在子线程中访问Unity的API:因为Unity的API不是线程安全的,在子线程中调用Unity的API会导致不可预测的错误,比如在子线程中修改GameObject的位置。

(2)线程同步:如果子线程和主线程需要共享数据,必须使用线程同步机制(如lock关键字、Monitor类等)来确保数据的一致性和安全性。

协程 VS 线程

对比维度协程(Coroutine)线程(Thread)
执行机制1. 运行在单线程中,依赖编程语言层面的调度(非操作系统调度)
2. 通过yield return主动暂停,后续在同一线程内恢复执行
3. 本质是 “协作式并发”,需手动让出执行权
4. 模拟并发效果,无真正并行(即使多核 CPU 也仅用单核心)
1. 由操作系统内核调度,可分配到多核 CPU 的不同核心
2. 抢占式执行(操作系统强制切换线程),无需手动暂停
3. 支持真正并行(多线程可同时在不同核心运行)
4. 属于 “抢占式并发”,线程切换由系统决定
资源消耗1. 创建/销毁开销极小(仅需维护代码执行上下文,如局部变量、程序计数器)
2. 不占用独立栈空间(共享所在线程栈)
3. 支持创建成千上万的协程而不影响性能
1. 创建/销毁开销较大(需分配独立栈空间、内核数据结构)
2. 每个线程默认栈空间通常为1MB~8MB(取决于系统)
3. 过多线程(如数百个)会导致内存占用激增、调度开销变大
数据共享与同步1. 同一线程内的协程共享数据时,无数据竞争(同一时间仅一个协程执行)
2. 无需锁、信号量等同步机制(除非跨线程调用协程)
3. 编程模型简单,不易出现死锁
1. 多线程共享进程内存时,易出现数据竞争(多线程同时操作同一数据)
2. 必须使用 lock、Monitor、Semaphore 等同步工具保证线程安全
3. 可能出现死锁、活锁等问题,调试难度高
适用场景1. 游戏中延迟操作(如延迟 2 秒显示提示、技能CD倒计时)
2. 分帧处理(如大数据列表加载、A*寻路分多帧计算,避免卡顿)
3. 等待异步事件(如等待网络请求响应、资源加载完成)
4. 轻量级循环任务(如帧更新内的阶段性逻辑)
1. CPU密集型任务(如大规模数据计算、视频编码/解码,利用多核提升效率)
2. IO密集型任务(如文件读写、网络请求,避免阻塞主线程)
3. 需长期后台运行的任务(如日志监控、数据同步服务)
调度控制权开发者通过yield return控制暂停/恢复时机,调度可控性强调度权完全由操作系统掌控,开发者无法预知线程切换时机,调度不可控
异常处理异常可在协程内部捕获,不直接影响其他协程(同一线程内需注意异常扩散)未捕获的线程异常可能导致线程崩溃,若主线程异常未处理会直接导致程序崩溃

       好了,本次的分享到这里就结束啦,希望对你有所帮助~


文章转载自:

http://BsgmmBK9.qkskm.cn
http://tnz2BxNz.qkskm.cn
http://aHdsEbJp.qkskm.cn
http://mZVzohpO.qkskm.cn
http://4AvFHXZR.qkskm.cn
http://dpGyWpF2.qkskm.cn
http://d2QDXn3O.qkskm.cn
http://575qU5iP.qkskm.cn
http://zyUGws20.qkskm.cn
http://AtcRym9h.qkskm.cn
http://BmqkhIrM.qkskm.cn
http://aD1biv4N.qkskm.cn
http://zyQq41C1.qkskm.cn
http://FxD6JlbQ.qkskm.cn
http://PJmU9Oai.qkskm.cn
http://XtpwV3lN.qkskm.cn
http://mhdi6FD1.qkskm.cn
http://7eWmQ1UV.qkskm.cn
http://eTpBr7VS.qkskm.cn
http://Rddm42zv.qkskm.cn
http://CLp2eryA.qkskm.cn
http://Wvrwfzyl.qkskm.cn
http://MBxxvJrk.qkskm.cn
http://w2LVVz1N.qkskm.cn
http://2VPariVM.qkskm.cn
http://jGxFlzQX.qkskm.cn
http://ZH8kH3AN.qkskm.cn
http://XPCwb7tC.qkskm.cn
http://ZqnO9FAp.qkskm.cn
http://tDVgE59b.qkskm.cn
http://www.dtcms.com/a/386665.html

相关文章:

  • 贪心算法在网络入侵检测(NID)中的应用
  • 数据搬家后如何处理旧 iPhone
  • [react native招聘]
  • IDE工具RAD Studio 13 Florence重磅发布:64 位 IDE + AI 组件全面升级!
  • session存储
  • Another Redis Desktop Manager 的 SCAN 使用问题与风险分析
  • MATLAB绘制一个新颖的混沌图像(新四翼混沌系统)
  • AI起名工具
  • typeScript 装饰器
  • 【算法磨剑:用 C++ 思考的艺术・单源最短路进阶】Bellman-Ford 与 SPFA 算法模板精讲,突破负权边场景
  • 单元测试:驱动模块与桩模块在自顶向下和自底向上的策略中的作用
  • SpringBoot MVC 快速入门
  • Nature Communications 北京大学联合德国马普所在触觉传感器方面取得进展,实现机器人指尖超分辨率力感知
  • 解决一次 “Failed to load model because protobuf parsing failed”:从现象到根因与修复
  • 从ppm到ppb:全面解读浓度单位转换的诀窍
  • 贪心算法应用:霍夫曼编码详解
  • NLP Subword 之 BBPE(Byte-level BPE) 算法原理
  • 【nodejs】Windows7系统下如何安装nodejs16以上版本
  • Part05 数学
  • 每天五分钟深度学习:深层神经网络的优势
  • PCGrad解决多任务冲突
  • 第十一章:游戏玩法和屏幕特效-Gameplay and ScreenEffects《Unity Shaders and Effets Cookbook》
  • Choerodon UI V1.6.7发布!为 H-ZERO 开发注入新动能
  • 科教共融,具创未来!节卡助力第十届浦东新区机器人创新应用及技能竞赛圆满举行
  • 食品包装 AI 视觉检测技术:原理、优势与数据应用解析
  • 【深度学习计算机视觉】05:多尺度目标检测之FPN架构详解与PyTorch实战
  • 从工业革命到人工智能:深度学习的演进与核心概念解析
  • [Emacs list使用及配置]
  • DQN在稀疏奖励中的局限性
  • 为何需要RAII——从“手动挡”到“自动挡”的进化