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

异步开发:协程、线程、Unitask

UniTask:https://github.com/Cysharp/UniTask

Lua源码协程的实现:https://blog.csdn.net/initphp/article/details/104296906

一文让你明白CPU上下文切换:https://zhuanlan.zhihu.com/p/52845869

协程

不同语言框架的协程

Unity 迭代器协程

  • 源码:
    • 创建、清理协程:MonoBehaviour.cppTryCreateAndRunCoroutine
    • 运行协程逻辑:Coroutine.cppRun->CallDelayed->ContinueCorutine
    • 驱动协程事件:Player.cppCallDelayed.cppCallDelayed
  • 大概逻辑,状态机,如下图:
    • 在这里插入图片描述
      在这里插入图片描述

    • 编译时:为每个协程创建一个类,包含需要用到的变量,每个yield记录为一个步骤(或状态),每次执行MoveNext就切换一次步骤。

    • 运行时:

      • 创建协程对象并执行TryCreateAndRunCoroutineCoroutine::Run执行。
        1. 会记录到MonoBehaviour自己的协程列表m_ActiveCoroutines中,用于Stop时候清理。
      • 执行MoveNext,如果返回false就结束清理协程,为true就继续。
      • 判断__current执行的函数类型,注册到DelayCallManager,等待下次时机再次触发。
        1. 比如yield return new WaitForSeconds(0.5f); 创建了一个回调,注册 kRunDynamicFrameRate``|kWaitForNextFrame类型事件,回调包含callbackCoroutine::ContinueCoroutine(就是执行第一步的Coroutine::Run)。
        2. // 不同类型触发事件时机不一样
          enum DelayedCallMode
          {kRunFixedFrameRate = 1 << 0,kRunDynamicFrameRate = 1 << 1,kRunStartupFrame = 1 << 2,kWaitForNextFrame = 1 << 3,kAfterLoadingCompleted = 1 << 4,kEndOfFrame = 1 << 5,kRunOnClearAll = 1 << 6,
          }
          
      • Player.cpp中,每帧update时触发GetDelayedCallmanger().Update(DelayedCallManager::``kRunDynamicFrameRate``),判断时间过了0.5秒后,触发回调。

C# Task async/await

  • 源码:
    • 创建Task,保存结果,处理异常:https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilder.cs
    • 处理线程调度,返回结果: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs
    • Task实现:https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs
  • 大概逻辑也是状态机驱动
    • 在这里插入图片描述

    • 比如:Task.Delay(1000),就是注册了计时器,1000毫秒后触发回调,重新执行MoveNext

      • 时间控制不受unity的时间影响,即使unity暂停了(Time.timeScale = 0),也会正常执行。
    • 每次await也是通过注册回调,等待触发时再执行MoveNext推进下一步。

Unity UniTask

  • 贴合 Unity 的 async/await 实现库。
  • 相比C#原生Task,性能更佳,并扩展常见unity异步操作(UnityWebRequestAssetBundleRequestIEnumerator )等。
  • 原理和async/await类似,UniTask为struct类型,Task为class类型,能做到0GC。
  • UniTask.Delay 在 PlayerLoopHelper驱动,类似Unity原生协程。

在这里插入图片描述

Lua 协程

  • 参考:https://blog.csdn.net/initphp/article/details/104296906
  • 和前面编译记录变量,走迭代器不同,lua是有栈协程:记录调用栈的信息,在恢复时,指针直接指回原来的调用函数和调用栈。
    • 语义像线程。
    • 要保存/恢复寄存器和栈,调度比无栈慢一些。
      在这里插入图片描述

有栈协程 VS 无栈协程

比较项有栈协程无栈协程
语言luaunity/c#
可跨函数 yield✅ 支持❌ 不支持
实现复杂度❌ 高(需管理栈)✅ 简单(状态机)
性能⬆️ 稍慢⬆️ 极快(切换仅跳转)
表达能力✅ 高❌ 受限
调试体验✅ 好❌ 较差
内存占用⬆️ 每个协程一个栈⬇️ 极小

协程对比

特性/对比项Unity 协程(IEnumerator)C# async/awaitUniTask(第三方)Lua 协程
使用语言C#C#C#(需引用 UniTask)Lua
返回值支持基本不支持返回值(靠回调)✅ 完整支持(Task)✅(支持 UniTask)✅(用 coroutine.yield())
多线程支持❌ 仅限主线程✅ 可用 Task.Run✅ UniTask.RunOnThreadPool❌ Lua VM 单线程
中断/恢复能力✅ 有 yield 控制权✅ 有 await 控制权✅ 类似 await✅ 用 yield/resume 控制
性能开销中等:GC 有开销高:Task 分配多低:几乎无 GC低:原生支持有栈协程
使用复杂度简单简单简单(需额外包)中等(需要状态管理)
错误处理不方便,需手动处理✅ 可用 try/catch✅ 同 async❌ 错误需 resume 判断
与 Unity 生命周期集成✅(StopCoroutine, yield WaitForSeconds)❌ 需要手动对接✅(支持 CancellationToken)❌ 不集成 Unity 生命周期
跨语言支持✅(可嵌入 Lua VM)

Unity协程开发建议

  • 优先考虑使用UniTask。
  • 如果有需要做同步等待操作,用Task,因为Unitask不直接支持。
    • 比如做Unity命令行工具,避免await直接跳出函数结束Unity进程。

线程

线程切换的完整流程

  • https://zhuanlan.zhihu.com/p/52845869
  • 下文大部分AI生成,谨慎观看

1. 当前线程被抢占或主动让出

  • 触发时机:
    • 时间片耗尽(抢占式调度)
    • 主动调用 yield / sleep
    • 阻塞等待(IO、锁、事件)
    • 线程优先级变化
    • 手动调用 Suspend(很少用)

2. 进入内核态,调度器开始工作

  • 系统中断(如时钟中断)或系统调用触发切换逻辑
  • 操作系统陷入 内核模式(Ring 0)
  • CPU 暂停当前线程,进入调度逻辑

3. 保存当前线程上下文

  • CPU 寄存器(如 EAX、EBX、ESP、EBP、RIP、RSP 等)
  • 程序计数器(PC/RIP)
  • 栈指针(ESP/RSP)
  • 条件码寄存器(FLAGS)
  • 协处理器状态(如浮点状态)
  • SIMD 寄存器(XMM、YMM) 保存位置:线程控制块(TCB)或内核栈

4. OS 选择新的线程

操作系统根据调度算法选择下一个可运行线程:

  • 时间片轮转(Round Robin)
  • 多级反馈队列
  • 优先级调度
  • CFS(Linux 完全公平调度器)

5. 加载新线程的上下文

  • 从新线程的 TCB 恢复:
    • 栈指针
    • 程序计数器
    • 寄存器组
    • SIMD 等扩展寄存器(如果用到)
  • 更新页表、内存映射(如果线程属于不同进程)

6. CPU 跳转到新线程继续执行

  • CPU 直接跳转到新线程的下一条指令(RIP)
  • 用户感知不到切换过程(除非主动测量)
  • 如果调度到的是新线程,则从入口开始执行

锁机制原理简述应用场景是否阻塞是否适用于主线程
lock(应用级)线程互斥执行,可能阻塞多线程资源共享,如下载、日志写入等✅ 是❌ 主线程慎用
Mutex(系统级)跨线程/进程锁,使用操作系统内核对象多进程资源访问(很少见)✅ 是
SpinLock自旋锁,忙等直到获得锁小粒度锁、短时间同步场景(如计数器)❌ 否
Interlocked原子操作,使用 CAS 实现原子加减、标志位更新❌ 否✅ 可用于主线程
ReaderWriterLockSlim支持读多写一的锁模型缓存共享读多的结构✅ 是
Semaphore(Slim)控制并发访问数量,Slim 为用户态轻量实现控制线程池并发数,加载队列等✅ 是

CAS(Compare And Swap)

  • 原子操作,对比旧值并原子替换
bool CAS(int* addr, int expected, int newVal) {if (*addr == expected) {*addr = newVal;return true;}return false;
}
  • 比如a=1,多线程执行a=a+1
int a = 1;
do {int oldValue = a;           // 读取当前值(例如为 1)int newValue = oldValue + 1;
} while (CAS(ref a, newValue, oldValue) != oldValue);
步骤线程 A线程 Ba 值说明
1读 a = 11A 准备执行
2读 a = 11B 也准备执行
3CAS(a, 2, 1) → 成功2A 把 a 改成 2
4CAS(a, 2, 1) → 失败2B 发现 a ≠ 1,CAS 失败 → 重试
5读 a = 22B 再次读取
6CAS(a, 3, 2) → 成功3B 把 a 改成 3

lock

  • C#最常见的锁lock(obj) {},会等obj这个锁对象用完才能继续执行。

Mutex

  • 跨进程锁,理解为不同应用之间的锁,比如wps在改表加锁,游戏也要改,通过这个锁来控制。

SpinLock

  • 自旋锁,通过CAS来拿到锁,再操作自己要操作的内容,用完释放锁。

Interlocked

  • 无锁,直接用CAS来操作自己要操作的内容,避免了加锁导致上下文切换。

ReaderWriterLock

  • 读写锁,常用于要读的频率比写的频率高很多时用
    • 读的时候计数+1,大于0时,不给写,但还可以继续读,计数一直+1,读完时计数-1。
    • 计数为0时,可以写。写的时候只能一个线程来写,不给别的线程读/写。

Semaphore

  • 比普通互斥锁多个计数,比如最多同时3个线程下载资源。

Unity中的多线程

  • Unity默认单线程开发。常见异步线程等待有请求URL、等待AB加载(解压序列化等)、IO读写等。
对比项\方式Threadasync/awaitUniTaskUnity Job System
使用原理操作系统级线程 + 栈/寄存器上下文切换,频繁切换有系统开销编译器将 async 方法编译成状态机类,await 注册回调恢复状态使用结构体状态机 + PlayerLoop 插入,避免GC,支持线程切换Job 为结构体任务描述,调度器统一派发,使用 Burst 编译器进行 SIMD 优化
线程模型✅ 原生系统线程(Win32/pthread)⛔ 默认主线程,基于 SynchronizationContext 决定✅ 可切换线程(主线程、线程池、任意自定义)✅ 多线程:由 Unity 的 Job 调度器在线程池中调度
是否多线程✅ 是⛔ 默认不是,除非配合 Task.Run 或 ThreadPool 使用⛔ 默认不是,支持 RunOnThreadPool 实现并发✅ 是:完全由 Unity 控制并行度
GC 生成情况高:每个线程分配堆栈,频繁创建回收会造成 GC 和系统开销中:状态机会产生堆对象,闭包捕获等也会增加 GC低:结构体实现状态机,无装箱;配合 Source Generator 基本为零 GC零 GC:Job 是结构体,使用 NativeArray 等 Native 数据结构
创建成本高:每个线程都要向 OS 申请资源,线程数过多影响性能低:状态机对象 + 回调注册极低:结构体、可复用对象池极低:结构体任务描述,调度器批量处理
线程控制粒度✅ 完全控制线程生命周期、调度逻辑⛔ 无法指定线程;调度逻辑受 SynchronizationContext 控制✅ 通过 SwitchToXxx() 指定在哪个上下文继续执行⛔ Job 完全由 Unity 控制,不可指定具体线程
是否能访问 Unity API❌ 否,Unity 只能在主线程访问其对象✅ 默认支持(await 后回到主线程)✅ 可自由切换主线程(支持 SwitchToMainThread())❌ 否:Job 不能访问任何托管对象、GameObject 等
依赖支持标准 .NET API标准 C# 语言特性UniTask(Cysharp)Unity DOTS 模块
调试难度中高:需要管理线程间同步与共享数据低:断点调试良好低:协程式语法调试友好中等:Job 排队、Burst 编译需特殊调试工具
适用场景需要自定义线程逻辑、长时间运行任务、第三方库依赖线程网络请求、IO异步、资源加载等Unity 环境中一切异步处理场景(替代 IEnumerator 协程)DOTS 数据密集型处理、大量 Entity 的计算逻辑
  • 接口区别:
功能/类别Threadasync/await (Task)UniTaskJob (IJob, IJobParallelFor)
创建方式new Thread(Action)async Task Func()async UniTask Func()MyJob : IJob { Execute() }
启动执行thread.Start()自动执行自动执行job.Schedule() 或 job.ScheduleParallel()
同步等待thread.Join()task.Wait() / GetResult()ToTask().Wait() 自身不支持,要转成TaskjobHandle.Complete()
返回值TaskUniTask通过结构体传值回主线程
取消支持手动控制变量CancellationTokenCancellationToken不支持取消(需要用户手动控制)
异常捕获需要手动 try-catch支持 try-catch(自动转异常)同 async/await不支持异常传播(需在 Job 内 catch)
调度在哪运行操作系统线程池(新线程).NET 线程池/主线程(取决于上下文)Unity PlayerLoop、线程池、主线程Unity Job System,工作线程池
多线程并发支持支持(通过 Task.Run)支持(通过 RunOnThreadPool)高效并发(推荐处理大量数据)
回到主线程方式手动调用 MainThreadDispatcher使用 SynchronizationContextUniTask.SwitchToMainThread()Complete() 后手动执行主线程逻辑

Unity多线程开发建议

  1. 用法区分:
    1. 如果是要长期自己维护一个线程用Thread。比如网络消息处理。
    2. 只是临时个别异步任务用UniTask,再考虑Task,最后再考虑IEnumerator
    3. 大量计算用Job。
  2. 异步线程开发不要忘了try-catch,有遇到过子线程抛异常没日志输出的情况,需要手动try-catch抛出。
  3. UnityEngine.Debug.Log 打日志不是线程安全的,遇到过编辑器子线程执行卡死,PC端执行卡死情况,安卓/iOS暂不清楚是否会有问题。

线程安全打印log

using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Experimental.PlayerLoop;// 简易的线程安全打日志,通过主线程手动DoUpdate驱动刷新日志
[ExecuteAlways]
public class DebugLoggerEx : MonoBehaviour
{private struct LogEntry{public string msg;public string stack;public LogType type;}private static readonly ConcurrentQueue<LogEntry> _logQueue = new ConcurrentQueue<LogEntry>();private static bool _isInit;#if UNITY_EDITOR[UnityEditor.InitializeOnLoadMethod]private static void EditorInit(){UnityEditor.EditorApplication.update -= ProcessLogs;if (Application.isPlaying)return;UnityEditor.EditorApplication.update += ProcessLogs;_isInit = true;}
#endifprivate static void Init(){if (!_isInit){var go = new GameObject("[DebugLoggerEx]");{DontDestroyOnLoad(go);}go.AddComponent<DebugLoggerEx>();_isInit = true;}}private static void LogInternal(string str, LogType type, bool stackTrace = true){Init();var entry = new LogEntry { msg = str, type = type };if (stackTrace)entry.stack = new System.Diagnostics.StackTrace(2, true).ToString();_logQueue.Enqueue(entry);}public static void LogSimple(string str){LogInternal(str, LogType.Log, false);}public static void Log(string str){LogInternal(str, LogType.Log);}public static void LogWarning(string str){LogInternal(str, LogType.Warning);}public static void LogError(string str){LogInternal(str, LogType.Error);}public static void ProcessLogs(){while (_logQueue.TryDequeue(out var log)){string fullMsg = $"{log.msg}\n{log.stack}";switch (log.type){case LogType.Warning: Debug.LogWarning(fullMsg); break;case LogType.Error: Debug.LogError(fullMsg); break;default: Debug.Log(fullMsg); break;}}}public void Update(){ProcessLogs();}
}
http://www.dtcms.com/a/333620.html

相关文章:

  • 线性代数 · 直观理解矩阵 | 空间变换 / 特征值 / 特征向量
  • 树莓派开机音乐
  • 模板引用(Template Refs)全解析2
  • CVE-2025-8088复现
  • 汽车行业 AI 视觉检测方案(二):守护车身密封质量
  • 【总结】Python多线程
  • 华清远见25072班C语言学习day10
  • 342. 4的幂
  • 自定义数据集(pytorchhuggingface)
  • 附046.集群管理-EFK日志解决方案-Filebeat
  • 考研复习-计算机组成原理-第七章-IO
  • NumPy基础入门
  • 第40周——GAN入门
  • 详解区块链技术及主流区块链框架对比
  • PSME2通过IL-6/STAT3信号轴调控自噬
  • 【机器学习】核心分类及详细介绍
  • 控制块在SharedPtr中的作用(C++)
  • 【秋招笔试】2025.08.15饿了么秋招机考-第二题
  • 基于MATLAB的机器学习、深度学习实践应用
  • Matlab(5)进阶绘图
  • 后端学习资料 持续更新中
  • StarRocks数据库集群的完整部署流程
  • plantsimulation中存储(store)、缓冲区(buffer)、放置缓冲区(PlaceBuffer)的区别,分别应用于那种情况
  • 第七十四章:AI的“诊断大师”:梯度可视化(torchviz / tensorboardX)——看透模型“学习”的秘密!
  • 测试用例的一些事项
  • API接口大全实用指南:构建高质量接口的六个关键点
  • Adobe Photoshop 2024:软件安装包分享和详细安装教程
  • Unity与OpenGL中的材质系统详解
  • 杭州电子商务研究院发布“数字化市场部”新部门组织的概念定义
  • Gato:多模态、多任务、多具身的通用智能体架构