【一文了解】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控制暂停/恢复时机,调度可控性强 | 调度权完全由操作系统掌控,开发者无法预知线程切换时机,调度不可控 |
异常处理 | 异常可在协程内部捕获,不直接影响其他协程(同一线程内需注意异常扩散) | 未捕获的线程异常可能导致线程崩溃,若主线程异常未处理会直接导致程序崩溃 |
好了,本次的分享到这里就结束啦,希望对你有所帮助~