区分同步(Synchronous)和异步(Asynchronous)
目录
区分同步(Synchronous)和异步(Asynchronous)
1.同步(Synchronous)
1.1.概念
1.2.同步在普通代码中的表现
1.3.同步在线程中的限制
2.异步(Asynchronous)
2.1.概念
2.2.实现异步的两种方式
2.2.1.协程(Coroutine):单线程内的异步模拟
1)协程的异步原理
2)协程异步的使用场景
3)示例:协程异步处理分帧任务
2.2.2.线程(Thread):多线程并行的异步
1)线程的异步原理
2)线程异步的使用场景
3)示例:线程异步读取文件
3.同步 VS 异步
4.协程异步 VS 线程异步(在Unity中的关键区别)
5.如何选择同步、协程异步、线程异步?
在Unity和C#中,同步(Synchronous)和异步(Asynchronous)是描述代码执行流程的核心概念,直接影响程序的响应性、性能和资源利用率。结合协程(Coroutine)和线程(Thread)的使用场景,可以更清晰地理解它们的区别和适用场景。
Unity和C#中的同步(Synchronous)和异步(Asynchronous)
1.同步(Synchronous)
1.1.概念
(1)概述:同步是“按顺序等待”的执行模式。
(2)定义:代码按照编写顺序依次执行,前一个任务未完成时,后续任务必须等待,直到前一个任务执行完毕才能继续。
(3)核心特点:单任务阻塞,执行流程直观但可能导致卡顿。
1.2.同步在普通代码中的表现
在Unity的Update、Start等方法中,默认都是同步执行
using System.Threading;
using UnityEngine;public class Test1: MonoBehaviour
{ private void Start(){Task1();//执行任务1(耗时2秒)Task2();//必须等待任务1完成后才执行}private void Task1(){Debug.Log("任务1开始");//模拟耗时操作(如大量计算)Thread.Sleep(2000);//阻塞主线程2秒Debug.Log("任务1完成");}private void Task2(){Debug.Log("任务2完成");}
}
执行结果:Task1执行时会阻塞主线程2秒,期间游戏画面卡顿,Task2必须等Task1完成后才执行。注意:若Task1是耗时操作(如文件读写、复杂计算),会直接导致游戏冻结,严重影响体验。
1.3.同步在线程中的限制
线程本身可以并行执行,但线程内部的代码默认是同步的
using System.Threading;
using UnityEngine;public class Test2: MonoBehaviour
{private void Start(){//启动子线程执行同步任务new Thread(SyncThreadTask).Start();}private void SyncThreadTask(){Step1();//必须完成后才能执行Step2Step2();}private void Step1() { /* 耗时操作 */ Debug.Log("Step1():执行");}private void Step2() { /* 依赖Step1的结果 */ Debug.Log("Step2():执行");}
}
子线程内部的Step1和Step2是同步执行的,但子线程的执行不会阻塞主线程(这是线程与同步的区别)。
2.异步(Asynchronous)
2.1.概念
(1)概述:异步是“不等待,先做其他事”的执行模式。
(2)定义:代码执行到耗时操作时,不阻塞当前线程,而是让线程继续处理其他任务,待耗时操作完成后再“回调”或“恢复”执行后续逻辑。
(3)核心特点:非阻塞,提高线程利用率,避免卡顿。全程线程不“同时做两件事”,只是利用“耗时操作的等待时间”做了其他任务,避免卡顿,提高效率。
用“主线程+协程加载资源”理解:
(1)初始执行:主线程执行协程代码,调用“异步加载资源”接口(比如Resources.LoadAsync<Texture2D>("image"))。
(2)委托耗时操作:主线程把“加载资源”这个耗时任务交给Unity的“资源加载管理器”(引擎层面的组件),然后“暂停协程的后续逻辑”。
(3)处理其他任务:主线程转而去执行“其他任务”(比如执行Update、渲染、UI交互),此时资源加载由“资源加载管理器”在后台处理,主线程完全不参与等待。
(4)恢复执行后续逻辑:当“资源加载管理器”完成加载后,会通知Unity的协程调度器;协程调度器在主线程的“下一个合适时机”(比如EndOfFrame或FixedUpdate之后),让主线程“恢复执行协程暂停后的代码”(比如把加载好的纹理赋值给Sprite)。
注意:
线程同一时间只能做一件事。整个过程中,主线程始终只做一件事:要么处理Update/渲染/UI交互,要么恢复协程后续逻辑,没有“同时加载资源和渲染”的情况。
2.2.实现异步的两种方式
在Unity和C#中,异步主要通过两种方式实现:协程(Coroutine)和多线程(Thread),但两者的异步机制有本质区别。
2.2.1.协程(Coroutine):单线程内的异步模拟
Unity协程是单线程异步的典型实现,依赖yield return关键字主动“让出执行权”,本质是“协作式异步”。
1)协程的异步原理
协程运行在Unity主线程中,不会创建新线程。通过yield return暂停自身执行,让主线程继续处理其他逻辑(如渲染、输入响应)。满足yield return的条件后(如等待2秒、等待帧结束),Unity会在下一帧从暂停处恢复协程执行。
2)协程异步的使用场景
①分帧加载:如“将10000条数据分10帧加载,避免单帧卡顿”。
②延迟操作:如“3秒后播放技能特效”。
③等待异步事件:如“等待资源加载完成后刷新UI”、“等待网络请求响应”
3)示例:协程异步处理分帧任务
using System.Threading;
using UnityEngine;public class Test3: MonoBehaviour
{private void Start(){List<string> largeDataList = new List<string>();for (int i = 0; i < 10000; i++){largeDataList.Add($"数据项 {i}");}StartCoroutine(ProcessLargeDataAsync(largeDataList));Debug.Log("协程已启动,主线程继续执行其他任务");//立即执行,不等待协程完成}private IEnumerator ProcessLargeDataAsync(List<string> dataList){int total = dataList.Count;int batchSize = 100;//每帧处理100条数据for (int i = 0; i < total; i += batchSize){//处理当前批次数据(同步执行,但只占一帧的部分时间)for (int j = i; j < Mathf.Min(i + batchSize, total); j++){ProcessData(dataList[j]);}//让出执行权,等待下一帧再继续(异步的核心)yield return null;Debug.Log($"已处理 {Mathf.Min(i + batchSize, total)}/{total} 条数据");}}private void ProcessData(string data){}
}
异步表现:ProcessLargeDataAsync执行时,每处理100条数据就暂停,主线程可以继续渲染画面、响应输入,避免卡顿。
2.2.2.线程(Thread):多线程并行的异步
线程是操作系统级别的异步,通过创建新线程实现“真正的并行”(多核CPU下),本质是“抢占式异步”。
1)线程的异步原理
线程由操作系统调度,可独立于主线程运行在不同CPU核心上。主线程无需等待子线程完成,两者可以并行执行(真正的异步)。子线程执行耗时操作(如文件读写、网络请求)时,主线程不受影响。
2)线程异步的使用场景
①CPU密集型任务:如大规模数据计算、图像处理(利用多核提升效率)。
②IO密集型任务:如读取大文件、下载网络资源(避免阻塞主线程)。
3)示例:线程异步读取文件
/// <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);}}
}
public class FileReaderThread : MonoBehaviour
{public TextMeshProUGUI resultText;//结果文本,显示文件内容private void Start(){Debug.Log("主线程开始,启动文件读取线程");//启动子线程执行异步文件读取new Thread(()=>ReadFileAsync(Path.Combine(Application.streamingAssetsPath, "file.txt"))).Start();Debug.Log("主线程继续处理其他任务(如UI更新)");//立即执行,不等待子线程}//子线程:异步读取大文件(不阻塞主线程)private void ReadFileAsync(string filePath){byte[] data = File.ReadAllBytes(filePath);//耗时IO操作Debug.Log($"子线程读取完成,文件大小:{data.Length}字节");//注意:子线程不能直接操作Unity API(如更新UI),需通过主线程调度器MainThreadDispatcher.EnqueueTask(() =>{resultText.text += $"文件:{Path.GetFileName(filePath)}\n内容:{File.ReadAllText(filePath)}\n\n"; ;//在主线程更新UI});}
}
异步表现:ReadFileAsync在子线程中执行耗时的文件读取,主线程可以继续处理UI渲染、用户输入,游戏画面保持流畅。
3.同步 VS 异步
对比维度 | 同步(Synchronous) | 异步(Asynchronous) |
---|---|---|
执行方式 | 按顺序执行,前一个任务阻塞后续任务 | 不阻塞,任务执行时允许其他操作并行/并发执行 |
线程关系 | 单线程内顺序执行,无线程切换 | 协程:单线程内通过yield切换任务 线程:多线程并行 |
阻塞风险 | 耗时任务会阻塞当前线程(如主线程卡顿) | 耗时任务不阻塞当前线程(协程让出执行权,线程独立运行) |
适用场景 | 简单逻辑、无耗时操作的任务(如变量赋值、简单计算) | 耗时操作(延迟、IO、计算)、需保持响应性的场景 |
Unity 限制 | 无特殊限制,但耗时操作会导致卡顿 | 协程:必须在主线程,可调用Unity API 线程:不可直接调用Unity API |
4.协程异步 VS 线程异步(在Unity中的关键区别)
特性 | 协程(Coroutine) | 线程(Thread) |
---|---|---|
线程数量 | 运行在Unity主线程,不创建新线程 | 创建新线程,独立于主线程 |
Unity API 调用 | 可以直接调用(如Instantiate、transform.position) | 禁止直接调用(会导致崩溃或异常),需通过主线程调度 |
数据安全 | 单线程内执行,无数据竞争问题 | 多线程共享数据时需加锁(lock),否则可能数据错乱 |
性能开销 | 极低(仅维护执行上下文) | 较高(线程创建、切换、内存占用) |
适用任务 | 短耗时、需与 Unity 生命周期交互的任务(如分帧加载、延迟操作、等待异步事件) | 长耗时、独立于Unity生命周期的任务(如CPU密集型任务、IO密集型任务) |
5.如何选择同步、协程异步、线程异步?
(1)同步:适用于无耗时操作的简单逻辑(如变量赋值、条件判断),直接顺序执行即可。
(2)协程异步:适用于需要与Unity交互且耗时较短的异步任务(如分帧加载、延迟操作、等待异步事件),优点是简单、安全,无需处理线程同步。
(3)线程异步:适用于完全独立于Unity生命周期且耗时长的任务,CPU密集型任务、IO密集型任务,优点是不阻塞主线程,但需注意线程安全和Unity API限制。
在实际开发中,三者往往结合使用,如用线程异步下载资源,下载完成后通过协程在主线程分帧加载资源并更新UI,既保证效率又避免卡顿。想要进一步了解进程、线程和协程可以查看【一文了解】Unity的协程(Coroutine)与线程(Thread)和区分进程(Process)、线程(Thread)和协程(Coroutine)
好了,本次的分享到这里就结束啦,希望对你有所帮助~