协程和异步

摘要: 本文介绍协程的基本概念,以及协程在异步IO编程模式里起的作用——大大简化异步回调的实现与逻辑处理。

什么协程

协程这个概念在计算机科学里算是一个老概念了,随着现代计算机语言与多核心处理器的普及,似乎也有普及之势。协程是与例程相对而言的。

熟悉C/C++语言的人都知道,一个例程也就是一个函数。当我们调用一个函数时,执行流程进入函数;当函数执行完成后,执行流程返回给上层函数或例程。期间,每个函数执行共享一个线程栈;函数返回后栈顶的内容自动回收。这就是例程的特点,也是现代操作系统都支持这种例程方式。

协程与例程相对,从抽象的角度来说,例程只能进入一次并返回一次,而协程可能进入多次并返回多次。比如说,我们有下面一段程序:

void fun(int val)
{
    int a=0; //1

    int b=0; //2

    int c=a+b; //3
}

如果上面的代码是一个例程,那么它只能把 1、2、3 依次执行后,才返回。如果是协程,它可能在 1 处暂停,然后在某个时刻从 2 处继续执行;接着在 2 处执行完之后暂停,然后在另外一个时刻从 3 处继续执行。

从抽象角度,协程就这么简单。

异步IO的特点与分析

在了解协程的特点(可以多次进入同一个函数,并接着上次运行处继续执行)后,我们再来考虑一下,这一特点如何应用到异步IO程序中。在异步IO程序中,有很大一块代码是处理异步回调的,也就是数据读取或写入由系统执行,当任务完成后,系统会执行用户的回调。如果只是很少使用这种回调,那么程序并不会因为异步而复杂多少,但要是程序中异步回调大量存在,那么此时我们会发现,原本简单的程序可能因为回调而变得支离破碎,原本一个简单的循环,现在需要写入多个函数,并在多个函数里来回调用。下面示例一下:

//下面代码片断是同步代码,它从IO读一段数据,并把这段数据写回
void start()
{
   for(;;)
   {
       Buffer buf;
       read (buf);//把书读到buf
       write(buf);//把buf的数据写回
    }

    //注意到没有,同步代码很简单直接,一个循环,几行代码完成全部事务
}

//把上面的同步代码映射为异步,代码量可能要增加很多,并且程序逻辑也变得不清晰
//示例如下

//读回调,在回调里我们发起写操作
void readHandle(buf)
{
    writeAsync(buf, writeHandle);
}

//写回调,在回调里我们发起读操作
void writeHandle(buf)
{
    readAsync(buf, readHandle);
}

//开始循环
void start()
{
    static Buffer buf; //buf变量不能在栈上,为了简单这里写成静态变量
    readAsync(buf, readHandle);
}

从上面的代码比较中,我们可以看出异步IO会把代码分隔成许多碎片,同时原本清晰的处理逻辑也因为被放入多个函数里,而变得很不清晰。上面的同步代码,一个了解程序的初级程序员也可以读懂写出,但相同功能的异步代码,一个初级程序员可能就搞不定了,甚至很难搞明白为什么要这么做。

读到这里,对异步不是太了解的人可能会问,既然异步把问题搞复杂了,那我们为什么还要用异步呢?答案简单有力,为了“性能”。只有这一个原因,当程序需要处理大量IO时,异步的效率将高出同步代码许多倍。如何一个程序的性能不其关心部分,那真不应该使用异步IO。

对比我们的异步IO代码与其功能相同的同步代码,我们发现每个异步调用都是要把代码分隔一个小函数——比原本要小的函数,当异步调用返回后,我们又接着下面处理。这一点跟协程很像,在一个协程里,当发起异步IO时,我让它返回,当异步IO完成后,我让这个协程接着执行,处理余下的逻辑。

协程与异步结合——性能与简单的结合

结合上面的分析,如果我们可以写下面功能的代码,将很完美:

void start()
{
    for(;;)
    {
        Buffer buf;

        yeild readAsync(buf,start);
    //------ 分隔线,协程在这里返回,等待readAsync完成,当readAsync完成后,它再调用start
    //此时start将从这里接着运行

        yeild writeAsync(buf, start);
    //------ 分隔线,协程在这里返回,等待writeAsync完成,当writeAsync完成后,它再调用start
    //此时start将从这里接着运行
    }
}

上面的代码也很一清晰明了。如果真的能写这样的代码,那将是所有程序员之福。这样在一些语言里确实可以直接写出来,比如Python、Lua、 golang,这些语言有原生的协程支持——编译器或解释器为我们处理了这些执行流的跳转。那么在C/C++里怎么实现呢?可能肯定的是,C/C++里不能直接写出这样简洁的代码,但却可以写类似的代码,尤其是C++——它提供了更强大的封装能力。

<think>嗯,用户问的是Unity中协程异步操作的区别。这个问题挺常见的,而且看起来用户是有基础的。他们特别提到协程(coroutine)异步(async)的比较,说明可能在项目里实际遇到了这个问题。之前系统指令提到要参考站内的两个引用,引用[1]强调Unity主线程单线程特性,引用[2]警告同步阻塞异步操作的问题。这两点确实很关键。首先得理清基本概念。协程在Unity里很常用,主要处理延迟执行的任务,像序列动画、分段加载资源这些。IEnumerator加上yieldreturn是标准写法。异步是C#的async/await语法,特别适合处理文件读写、网络请求这类I/O密集操作。关键区别在于执行原理:协程是靠Unity主循环驱动的,异步任务通过状态机运行在主线程上,都不需要额外线程。这点必须讲清楚,因为用户可能误以为async是多线程,而引用[1]正好提醒Unity单线程的特性。然后想到用户可能的实际痛点:他们应该是在犹豫某个功能到底该用协程还是异步,特别是碰到耗时操作时的卡顿问题。比如文件加载,用协程yieldreturn虽然不会完全卡死画面,但实际主线程仍然在同步处理,无法响应其他操作。这个场景就需要async/await的异步I/O优势。还有停止机制的区别也很实用——协程StopCoroutine直接打断,而async中断需要CancellationTokenSource配合,用户肯定遇到过需要中途取消的情况。性能考虑要重点提引用[2]的警告:await结果时用.Result阻塞会导致更严重卡顿。这正好对应引用[2]的"frameratehitches"风险。这里适合举反例:很多人图省事在协程里跑同步Http请求,实际比await异步请求更卡。最后用户提问质量不错,问题很聚焦。可能需要知道深层次的应用场景区别,比如异步操作更适合连接网络资源。所以生成的扩展问题里包含应用场景、资源管理这些实操性强的方向,后续讨论就能更深入。</think> 在 Unity 中,**协程(Coroutines)** **异步操作(async/await)** 都是处理耗时任务而不阻塞主线程的工具,但实现机制适用场景存在核心差异: --- ### **1. 协程(Coroutines)** - **原理**: 基于 `IEnumerator` 迭代器,通过 `yield` 关键字暂停执行,**依赖 Unity 主线程驱动**。协程的恢复由 Unity 主循环(如 `Update` 后)触发。 - **线程模型**: 完全运行于 **Unity 主线程** 上,本质是 **分帧执行**(例如 `yield return null` 等待下一帧)。 - **语法**: ```csharp IEnumerator MyCoroutine() { Debug.Log("Start"); yield return new WaitForSeconds(1); // 等待1秒(不阻塞主线程) Debug.Log("End"); } ``` - **特点**: - ✅ **简单易用**:适合序列化任务(如动画、分段加载)。 - ✅ **无缝访问 Unity API**:所有代码均在主线程执行。 - ❌ **无法跨帧保存状态**:暂停后局部变量被保留,但若脚本被禁用/销毁则中断。 - ❌ **不支持后台计算**:CPU 密集型任务仍会卡顿主线程。 --- ### **2. 异步操作(async/await)** - **原理**: 基于 C# 的 `Task` 异步编程模型,通过状态机实现非阻塞等待。**不创建新线程**,但可将 I/O 任务(如网络请求)移交系统底层处理。 - **线程模型**: 默认运行于 **Unity 主线程**([引用[1]](^1))。使用 `ConfigureAwait(true)` 确保回到主线程上下文。 - **语法**: ```csharp async Task LoadData() { Debug.Log("Start"); await Task.Delay(1000); // 异步等待1秒 Debug.Log("End"); // 自动回到主线程 } ``` - **特点**: - ✅ **高效处理 I/O 任务**:如下载、文件读写(利用系统异步 I/O)。 - ✅ **链式调用**:支持组合多个异步操作(`async/await` 链)。 - ❌ **谨慎使用阻塞操作**:同步阻塞(如 `.Result`)会导致主线程卡顿([引用[2]](^2))。 - ❌ **部分 Unity API 限制**:需在主线程调用的 API 需搭配 `MainThreadDispatcher`。 --- ### **核心区别总结** | **特性** | 协程 (Coroutine) | 异步 (async/await) | |------------------------|-------------------------------|--------------------------------| | **底层机制** | Unity 迭代器驱动 | C# 状态机驱动 | | **线程占用** | 始终在主线程执行 | 主线程执行(默认) | | **适用场景** | 分帧任务、动画序列 | I/O 密集型任务(网络/文件) | | **暂停恢复** | 通过 `yield` 控制 | 自动状态机跳转 | | **中断方式** | `StopCoroutine()` | `CancellationToken` | | **性能优化** | 适合轻量级任务 | 减少 I/O 等待时间 | --- ### **何时选用?** 1. **协程更优**: - 需要逐帧控制流程(如移动、动画)。 - 简单延迟操作(`WaitForSeconds`)。 - 依赖 Unity 生命周期的事件(如 `StartCoroutine` 在 `Start()` 中调用)。 2. **异步更优**: - 网络请求(`UnityWebRequest` + `async`)。 - 本地文件读写(`System.IO.File.ReadAllTextAsync`)。 - 复杂异步逻辑组合(如并行下载 + 解析)。 --- ### **注意事项** - **避免混合使用**:协程中调用 `async` 可能引发嵌套复杂性(推荐二选一)。 - **异步资源加载**:优先用 `Addressables.LoadAssetAsync`,而非协程轮询。 - **主线程安全**:异步操作后需操作 Unity 对象时,确保回到主线程(可用 `MainThreadDispatcher`)。 > **引用[1]** 强调 Unity 单线程特性,协程异步均在此框架下运行;**引用[2]** 警告异步操作阻塞会导致卡顿,两者均需避免在主线程同步等待。 --- ###
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值