今天听了猫大直播时对Fiber的讲解。再结合这几天看过的同步上下文知识。说一下我对Fiber的看法。如果哪里理解错了,也请大家帮我指出来,不然孩子会在错误的理解上一路走到黑。
ThreadPoolScheduler
负载均衡
假如我们有四个核,可以分配出10个线程。现在我们创建了1000个Fiber。那线程调用Fiber的逻辑是:
计算自己一次处理Fiber的任务数量
从id队列中取出对应的Fiber1,执行Fiber1的任务。
Fiber1执行完之后,再从id队列中取出下一个Fiber2执行。
如果id队列空了,线程休息。
这样设计对负载均衡是友好的。假如线程1处理的Fiber1很耗时,则线程1被卡住,但是线程2,线程3可以继续从id队列中取出Fiber去执行。
如果我们1000个Fiber,在创建的时候就分配给指定的线程。类似这样,线程1处理Fiber1-100,线程2处理Fiber101-200,以此类推,最后线程10处理Fiber901-1000。
那假如我1000个fiber里,就Fiber1特别耗时,那我线程1在处理Fiber1的时候,Fiber2-100,就得等待Fiber1执行完才会被处理到。这样,就会导致不同线程分配不均的情况,毕竟,我们在开发的时候,并不知道每个Fiber的耗时,不能够说我知道Fiber1很耗时,那我线程1就只负责Fiber1和其他10个简单的Fiber。
SynchronizationContext.SetSynchronizationContext
那为什么每次调用Fiber.Update之前,都要先SynchronizationContext.SetSynchronizationContext(fiber.ThreadSynchronizationContext);
呢。
可以先看一下[SynchronizationContext](app://obsidian.md/SynchronizationContext)。
在ET中,因为是多线程开发,所以我们需要考虑多线程的安全问题。
例如:(AI举例)
竞态条件(Race Condition):当多个线程同时访问和修改共享数据时,可能会导致竞态条件。为了避免竞态条件,需要使用同步机制(如锁、互斥量、信号量等)来保护共享数据的访问。
死锁(Deadlock):当多个线程相互等待对方释放资源时,可能会发生死锁。为了避免死锁,需要避免循环等待和合理地管理资源的获取和释放顺序。
数据共享和同步:在多线程编程中,共享数据需要进行适当的同步,以确保线程安全。可以使用锁、互斥量、条件变量等同步机制来保护共享数据的访问。
线程安全性:需要注意并发环境下的线程安全性。确保多个线程可以正确地访问和修改共享数据,而不会导致不一致或错误的结果。
线程间通信:在多线程编程中,线程之间需要进行通信和协调。可以使用线程间的信号量、事件、条件变量等机制来实现线程间的通信。
上下文切换开销:多线程编程会引入上下文切换的开销。如果线程数量过多或上下文切换频繁,可能会导致性能下降。需要合理地管理线程数量和调度策略,以提高性能。
异常处理:在多线程编程中,需要注意异常处理。确保及时捕获和处理异常,以避免线程终止或未处理的异常导致程序崩溃。
那我们直接让同一个Fiber的任务,同时只能让一个线程去处理,就可以做到线程同步,也不会有线程安全的问题。
我们前面说到,ET的Fiber是由不同线程去获取并处理的,所以同一时间,Fiber具体在哪个线程,我们并不知道。可能我Fiber1在线程1发起的异步任务task1,等task1执行完之后,FIber1已经是在线程n上处理了。
这个时候,如果我不做额外处理,.Net的await/async,会把func1之后的逻辑,在完成func1的线程x去处理。但这时Fiber1是在线程2中处理的,这样,线程x和线程n同时在处理Fiber1,就容易引发线程的安全问题。
而在 [SynchronizationContext](app://obsidian.md/SynchronizationContext)中,我们知道,当异步方法完成时,如果想返回指定线程去执行,可以通过SynchronizationContext
的Post
方法。
ET实现了一个自定义的 [ThreadSynchronizationContext](app://obsidian.md/ThreadSynchronizationContext),可以将需要返回指定线程执行的”回调“方法,存到一个线程安全的队列里。
然后在每个Fiber的LateUpdate
当中,去处理这个队列里的"回调"。这样,能保证队列中的方法都是在同一线程当中去执行的。
那怎么让await Task.Run后面的"回调",存进这个队列呢?
出现这个问题的原因是,我们在执行以下代码时
await Task.Run(Action1)
dosomething
async/await是语法糖,会在IL代码里生成一个状态机。
返回值为类Task 且还有async前缀的方法内部 会构造状态机
异步方法内部遇到await的时候,会拆出一个状态。
例如上面的代码,就会有一个状态: 等待 Task.Run(Action1) 完成的状态。
而 Task.Run(Action1) 完成后,会通知这个状态机退出等待状态,进入下一个状态,也就是后续的dosomething。
每个可await的任务,都必须要实现GetAwaiter方法,返回与任务关联的Awaiter。
而我们可以通过这个Awaiter,将任务与当前状态机关联在一起,等任务执行完后,继续执行状态机。也就是调用到IAsyncStateMachine.MoveNext。
而Awaiter是怎么实现的,任务执行完后又是怎么通知到状态机的,是可以自定义的。
.Net Core的Task,Task就实现了自己的一套。里面有复杂的上下文流动、切换、线程池、调度器的逻辑。
ETTask则是一个简洁更加高效的实现。
可以参考[ETAsyncTaskMethodBuilder](app://obsidian.md/ETAsyncTaskMethodBuilder) 和 [ETTask](app://obsidian.md/ETTask)源代码。
.Net的Task是怎么处理的呢?
下面是我的简单理解,想深入了解的可以去查阅资料。
这个通知的行为,被Awaiter包装成一个回调,与当前的任务关联。
一般来说,任务被丢到线程池里,供空闲线程去执行。执行完后,会调用到关联的这个回调,继续状态机的执行。
而这个回调在被创建时,会根据需求和环境生成不同类型的回调
SynchronizationContextAwaitTaskContinuation
TaskSchedulerAwaitTaskContinuation
AwaitTaskContinuation
普通的Action。
一般来说,我们会生成普通的Action,这样,就会被任务执行完的当前线程,继续我们的状态机。
我们要的,就是 SynchronizationContextAwaitTaskContinuation,将通知状态机继续执行的行为,Post到指定的SynchronizationContext当中。
这时候,Unity客户端开发的同学发现问题了,不对啊,我Unity里面调用 await Task.Run后,打印当前线程还是主线程啊。
Unity自己做过处理,会在主线程设置UnitySynchronizationContext
https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Scripting/UnitySynchronizationContext.cs#L99
// SynchronizationContext must be set before any user code is executed. This is done on
// Initial domain load and domain reload at MonoManager ReloadAssembly
[RequiredByNativeCode]
private static void InitializeSynchronizationContext()
{
var synchronizationContext = new UnitySynchronizationContext(System.Threading.Thread.CurrentThread.ManagedThreadId);
SynchronizationContext.SetSynchronizationContext(synchronizationContext);
Awaitable.SetSynchronizationContext(synchronizationContext);
}
这样,在主线程发起的await Task,会生成SynchronizationContextAwaitTaskContinuation回调,这种类型的回调,会将回调的方法用SynchronizationContext.Post的方法推送到对应上下文的任务队列里。而UnitySynchronizationContext维护一个自定义任务队列,供unity主线程去执行。这样,就能保证主线程发起的await task任务结束后,会在主线程继续执行状态机。
而ET的ThreadSynchronizationContext,也可以起到类似的作用。当然,有且不止这个做用,我们可以手动调用Post来做一些定制化的需求。
—
关于异步操作,以及await/async的分析,我讲的不全对。这里贴出几篇文章供大家参考。文章内容对错自行判断。
How Async/Await Really Works in C# - .NET Blog (microsoft.com)
Async/Await在 C#语言中是如何工作的 | .NET中文官方博客 (microsoft.com)
.NET Task 揭秘(1):什么是Task - 知乎 (zhihu.com)
理解 C# 中的 async await-CSDN博客
C#中的ExecutionContext 和 SynchronizationContext - 知乎 (zhihu.com)