根据设计,从粒度代码生成的任何子任务(例如,使用await
或ContinueWith
Task.Factory.StartNew
)按与父任务相同的每次激活TaskScheduler调度。 因此,它们继承与粒度代码的其余部分相同的 单线程执行模型 。 这就是基于 grain 轮次的并发性的单线程执行幕后的要点。
在某些情况下,粒度代码可能需要从Orleans 任务计划模型中 “突破” 并 “执行特殊操作”,例如显式将 Task
指向不同的任务计划程序或 .NET ThreadPool。 例如,当粒度代码需要执行同步远程阻塞调用(如远程I/O)时。 在粒度上下文中执行该阻塞调用会阻止粒度,因此不应执行。 相反,粒度代码可以在线程池线程上执行此阻塞代码片段,联接 (await
) 完成该执行,然后在粒度上下文中继续。
Orleans调度器的脱离被认为是一个非常高级且很少需要的使用场景,超出典型的使用模式。
基于任务的 API
await
、 TaskFactory.StartNew (见以下)、Task.ContinueWith、Task.WhenAny和Task.WhenAllTask.Delay所有都遵循当前任务计划程序。 这意味着以默认方式使用它们,而不传递其他 TaskScheduler方法会导致它们在粒度上下文中执行。Task.Run 以及
endMethod
的 TaskFactory.FromAsync 委托都不遵循当前任务计划程序。 它们都使用计划TaskScheduler.Default
程序,即 .NET 线程池任务计划程序。 因此,Task.Run
和endMethod
中的代码始终在 .NET 线程池上运行,运行在 Orleans grain 的单线程执行模型之外。 在await Task.Run
或await Task.Factory.FromAsync
之后的任何代码,都会在任务创建时活动的计划程序下运行,而这正是粒度的计划程序。值为 Task.ConfigureAwait 的
false
是用于摆脱当前任务计划程序的显式 API。 它会导致等待后的代码在Task
上的 TaskScheduler.Default 调度程序(.NET 线程池)上执行,从而打破了对 "grain" 的单线程执行的保证。注意
通常, 切勿
ConfigureAwait(false)
直接在粒度代码中使用。带有签名
async void
的方法不应与 grain 一起使用。 这些方法适用于图形用户界面事件处理程序。 一种async void
方法如果允许异常未被处理直接抛出,则该方法可能导致当前进程立即崩溃,而无法处理该异常。 这也适用于List<T>.ForEach(async element => ...)
和任何接受 Action<T> 的其他方法,因为异步委托转换为async void
委托。
Task.Factory.StartNew
和 async
委托
在 C# 中计划任务的通常建议是使用 Task.Run
而不是 Task.Factory.StartNew
。 快速 Web 搜索Task.Factory.StartNew
表明它很危险,并且总是推荐倾向于Task.Run
。 但是,若要遵循此粒度的单线程执行模型,就必须使用Task.Factory.StartNew
。 那么,如何正确使用它?
Task.Factory.StartNew()
的危险在于它缺乏对异步委托的原生支持。 这意味着像 var notIntendedTask = Task.Factory.StartNew(SomeDelegateAsync)
这样的代码可能是一个 bug。
notIntendedTask
不是在SomeDelegateAsync
完成时完成的任务。 相反, 请始终 解包返回的任务: var task = Task.Factory.StartNew(SomeDelegateAsync).Unwrap()
。
示例:多个任务和任务计划程序
下面的示例代码演示了如何使用 TaskScheduler.Current
和 Task.Run
的特殊自定义调度程序来逃逸 Orleans 粒度环境,以及如何返回到该环境。
public async Task MyGrainMethod()
{
// Grab the grain's task scheduler
var orleansTS = TaskScheduler.Current;
await Task.Delay(10_000);
// Current task scheduler did not change, the code after await is still running
// in the same task scheduler.
Assert.AreEqual(orleansTS, TaskScheduler.Current);
Task t1 = Task.Run(() =>
{
// This code runs on the thread pool scheduler, not on Orleans task scheduler
Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
Assert.AreEqual(TaskScheduler.Default, TaskScheduler.Current);
});
await t1;
// We are back to the Orleans task scheduler.
// Since await was executed in Orleans task scheduler context, we are now back
// to that context.
Assert.AreEqual(orleansTS, TaskScheduler.Current);
// Example of using Task.Factory.StartNew with a custom scheduler to escape from
// the Orleans scheduler
Task t2 = Task.Factory.StartNew(() =>
{
// This code runs on the MyCustomSchedulerThatIWroteMyself scheduler, not on
// the Orleans task scheduler
Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
Assert.AreEqual(MyCustomSchedulerThatIWroteMyself, TaskScheduler.Current);
},
CancellationToken.None,
TaskCreationOptions.None,
scheduler: MyCustomSchedulerThatIWroteMyself);
await t2;
// We are back to Orleans task scheduler.
Assert.AreEqual(orleansTS, TaskScheduler.Current);
}
示例:从线程池线程上运行的代码进行粒度调用
另一种情况是,Grain代码需要“突破”Grain的任务调度模型,并在线程池线程(或其他一些非Grain上下文)上运行,但仍然需要调用另一个Grain。 可以从非 grain 上下文发出 grain 调用,而无需进行额外的准备。
以下代码演示如何从在粒度内运行但不在粒度上下文中运行的代码进行粒度调用。
public async Task MyGrainMethod()
{
// Grab the Orleans task scheduler
var orleansTS = TaskScheduler.Current;
var fooGrain = this.GrainFactory.GetGrain<IFooGrain>(0);
Task<int> t1 = Task.Run(async () =>
{
// This code runs on the thread pool scheduler,
// not on Orleans task scheduler
Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
int res = await fooGrain.MakeGrainCall();
// This code continues on the thread pool scheduler,
// not on the Orleans task scheduler
Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
return res;
});
int result = await t1;
// We are back to the Orleans task scheduler.
// Since await was executed in the Orleans task scheduler context,
// we are now back to that context.
Assert.AreEqual(orleansTS, TaskScheduler.Current);
}
使用库
代码使用的一些外部库可能在内部使用 ConfigureAwait(false)
。 实现常规用途库时,使用ConfigureAwait(false)
在 .NET 中是一种良好且正确的做法。 这在Orleans不是问题。 只要调用库方法的粒度代码使用常规 await
等待库调用,则粒度代码是正确的。 结果完全如预期:库代码在默认计划程序上运行延续(返回 TaskScheduler.Default
的值不保证延续在线程上运行,因为它们通常内联在上一 ThreadPool 个线程上),而粒度代码在粒度计划程序上运行。
另一个常见问题是库调用是否需要用 Task.Run
执行—也就是说,库代码是否需要显式卸载到 ThreadPool
(例如 await Task.Run(() => myLibrary.FooAsync())
)。 答案是“否”。 除非库代码进行阻塞同步调用,否则无需将代码卸载到 ThreadPool
。 通常,任何编写良好的 .NET 异步库(其方法返回 Task
并以 Async
作为后缀命名)都不会进行阻塞调用。 因此,除非怀疑异步库存在 bug 或故意使用同步阻塞库,否则无需将任何内容卸载到 ThreadPool
。
死锁数
由于粒度是单线程执行的,因此通过需要多个线程来解锁的方式同步阻塞粒度可能会导致死锁。 这意味着调用以下任一方法和属性的代码可以在调用方法或属性时未完成提供的任务的粒度死锁:
Task.Wait()
Task.Result
Task.WaitAny(...)
Task.WaitAll(...)
task.GetAwaiter().GetResult()
避免这些方法在任何高并发服务中,因为它们可能会导致性能不佳和不稳定。 它们通过阻止可能执行有用工作的线程来耗尽 .NET ThreadPool
,并要求 ThreadPool
为完成注入其他线程。 执行粒度代码时,这些方法可能会导致粒度死锁,因此也可以在粒度代码中避免它们。
如果某些 同步-异步 工作是不可避免的,那么最好将这些工作移到一个单独的调度器上。 例如,最简单的方法是使用 await Task.Run(() => task.Wait())
。 请注意,强烈建议避免 同步过度同步 工作,因为这会损害应用程序可伸缩性和性能。
总结:在 Orleans 中处理任务
正在尝试执行哪种操作? | 如何实现 |
---|---|
在 .NET 线程池线程上运行后台工作。 不允许使用 grain 代码或 grain 调用。 | Task.Run |
使用基于 Orleans 轮次的并发性保证(参阅上文)从 grain 代码运行异步工作线程任务。 | [.] |
使用基于 Orleans 轮次的并发性保证从 grain 代码运行同步工作线程任务。 | Task.Factory.StartNew(WorkerSync) |
工作项执行超时 | Task.Delay + Task.WhenAny |
调用异步库方法 |
await 库调用 |
使用 async /await |
常规 .NET 任务异步编程模型。 支持且建议使用 |
ConfigureAwait(false) |
不要使用内部 grain 代码。 只允许在库中使用。 |