异步与状态机(Async与 StateMachine)

协程其实只是模拟“并发”,本质上是利用时间片轮转的机制,实现看起来像是函数之间两不耽误。

异步在“并发”方面往往比协程更加出色和灵活,可以借助C#的“线程池”来做到真正意义上的“并发”。

public class AsyncTest : MonoBehaviour 
{
  async void Start()
  {
    Debug.Log("Start");
    
    await AsyncMethod(); 

    Debug.Log("End");
  }

  async Task AsyncMethod()
  {
    Debug.Log("Async Method Start");

    await Task.Delay(1000);

    Debug.Log("Async Method End"); 
  }
}

async关键字一般用于标记一个函数是异步函数,添加async关键字后,在函数内部可以使用await关键字来暂停函数的执行并等待一个任务结果的完成。

AsyncMethod是一个异步方法,当我们调用它时,我们会立刻得到一个"Async Method Start"的调试信息,然后这个方法会暂时搁置,并等待「 Task.Delay(1000) 」完成才会继续,然后再得到"Async Method End"的调试信息。

与同步函数不同的是,await Task.Delay(1000)这一行是会异步等待1秒,而不会阻塞主线程,主线程在调用该方法后,会在异步操作await处返回,去执行其他代码,不会被阻塞

Task[] tasks = new Task[]  
{
  SomeAsyncTask1(),
  SomeAsyncTask2(),
  SomeAsyncTask3() 
};

await Task.WhenAll(tasks);

更加直观的异步效果是类似于上面这种情况,我们会直接开启三个任务,然后让它们同时执行并且彼此互不干扰,也就是不等待SomeAsyncTask1返回就开始执行SomeAsyncTask2,同样也不等SomeAsyncTask2返回就开始执行SomeAsyncTask3,最后使用await Task.WhenAll(tasks)来一起等待三个任务全部完成。

在C#中,Task代表.NET任务/工作单元这个概念,它代表一个正在执行或者已经执行完成的操作,我们可以把它比作一个“任务单”来理解。

  1. 当方法返回void时,就像提交任务后立即离开,无法获知任务结果,也无法监控任务的进展。
  2. 当方法返回Task时,相当于获得了这个“任务单”,可以监控任务的进展。

Task可以封装任意异步操作,如IO、网络、计算等,并以统一的方式表示。

Task返回的是任务对象,可以使用链式语法启动任务并继续添加操作。

Task有Pending、Running、Completed、Faulted、Canceled等状态,可以判断任务执行情况。

using System;
using System.Threading.Tasks;

class Program
{
  static async Task Main() 
  {
    // 创建一个任务
    Task task = Task.Run(() => DoWork());  

    // 主线程立即继续执行    
    Console.WriteLine("Main thread doing work...");

    // 等待任务完成,但不阻塞主线程
    await task;

    Console.WriteLine("Task completed.");

    Console.ReadKey();
  }

  static async Task DoWork()
  {
    Console.WriteLine("Task starting work...");

    await Task.Delay(2000); // 使用Task.Delay实现非堵塞等待

    Console.WriteLine("Task finished work."); 
  }
}

也可以直接通过Task task = Task.Run(()=>DoWork())创建一条任务,并指定任务的具体内容。「 Task.Run() 」 它会开启新的线程来运行内部的代码。C# 中的「 Task 」是否要为其开启一个新的线程,这主要由Task的创建方式决定,async/await默认复用现有线程,而Task.Run/StartNew则明确指定新线程,其他情况下,Task不会主动开启新线程,而是由调用方指定运行模式。

只要你想干的事情需要依赖于MonoBehaviour类,在非主线程中是无法进行的,因为Unity要实现它的线程安全。

这里就要提到“状态机”的概念,事实上游戏中的很多人物状态都是通过状态机来控制的。

Task对象在底层是通过状态机实现的。编译器会隐式将Task对象转化为状态机。

Task对象有多个状态,比如运行中(Running)、暂停(Suspended)、完成(Completed)等。

执行Task的内部任务时,Task对象进入运行中状态。

遇到await表达式时会暂停Task对象,状态变为暂停,这个过程会释放线程资源,让其他代码运行。

当你标记一个方法为async时,你可以在该方法中使用await关键字。当编译器看到await关键字时,它会将该方法分割成几个部分。这些部分被组织成一个状态机。当运行到await表达式时,如果该操作已经完成,那么它会继续直接运行。如果操作尚未完成,那么它会将剩余的方法包装成一个回调,并且返回一个Task给调用者。

Task对象的任务完成后,状态机会自动恢复该Task对象的运行状态。异常也会作为状态机的“错误”状态来捕获。Task对象可以包含多个await,多次变换运行-暂停状态。

当函数被调用时,会创建一个新的执行上下文压到调用栈上。函数执行完成后,也就是return返回值之后,上下文会从栈中移除。

当await暂停一个异步函数时,它的执行上下文仍然存在于调用栈中。等暂停的操作完成后,会使用同一个上下文恢复函数的执行。

这保证了函数在暂停和恢复时能够访问到它执行期间的所有重要信息。这点其实和「 协程(Coroutine)」是一样的,在协程中,当协程启动时,迭代器函数被放入一个等待队列,当遇到yield关键字时,协程被挂起,但执行上下文保持在调用栈中,上下文中包含该协程所有变量、方法状态等信息,Unity引擎每帧从等待队列取出一个IEnumerator进行执行,执行到上次yield点继续向后执行,调度循环如此不断交替不同协程的执行。

异步函数相比协程的优势:程需要通过轮询方式从协程管理器中取出迭代器函数进行执行。队列的增加和移除带来额外开销,此外,每个协程都需要保留其上下文信息。当任务量很大时,上下文的创建和销毁也会影响性能,同时不断地遍历队列也会带来更加明显的开销。当任务数量大到几百/几千级别时,协程的性能会明显下降。异步函数可以很好地利用多核CPU资源,提高吞吐量,而协程只能在单个线程中顺序执行,难以充分利用多核优势。但是由于Unity的线程保护机制,异步函数无法操作游戏内物体,一般出现在资源、网络、场景等关键词下。

全部评论

相关推荐

玉无心❤️:发照片干啥 发简历啊
点赞 评论 收藏
分享
评论
2
2
分享

创作者周榜

更多
牛客网
牛客企业服务