title: 外部任务和Grains

在Orleans的设计里,任何从Grain代码中产生的子任务(例如,通过awaitContinueWithTask.Factory.StartNew产生的)将在与父任务相同的 per-activation TaskScheduler上进行调度,从而继承了与其他Grain代码相同的单线程执行模型。 这是Grain基于回合的并发的单线程执行背后的基点。

在某些情况下,Grain代码可能需要“突破”Orleans的任务调度模型去”做一些特别的事情”,比如将一个Task显式指向不同的任务调度器或.NET线程池。 例如,Grain代码必须执行一个同步的远程阻塞调用(如远程IO)。 在Grain上下文中执行该阻塞调用将阻塞Grain,因此不应该这样做。 相反,Grain代码可以在线程池线程上执行这段阻塞代码,并join(await)该执行的完成,并在Grain上下文中继续。 我们期望逃逸出Orleans调度器将是一个非常高级的且很少需要的,超出“正常”使用模式的使用场景。

基于Task的API

  1. awaitTask.Factory.StartNew(见下文)、Task.ContinueWithTask.WhenAnyTask.WhenAllTask.Delay都遵循当前任务调度器。 这意味着在不传递不同的任务调度器的情况下,以默认方式使用它们将使它们在Grain上下文中执行。

  2. Task.RunTask.Factory.FromAsyncendMethod委托都遵循当前任务调度器。它们都使用TaskScheduler.Default调度器,即.NET线程池任务调度器。因此,Task.Run内的代码和Task.Factory.FromAsync内的endMethod始终运行在Orleans Grain的单线程执行模型之外的.NET线程池上,详见这里。但是,在await Task.Runawait Task.Factory.FromAsync之后的所有代码将在任务创建时的调度器下运行,也就是Grain的调度器。

  3. ConfigureAwait(false)是一个用来显式逃逸出当前任务的调度器的API。它将使等待任务后的代码在TaskScheduler.Default调度器上执行,也就是.NET线程池,因此会破坏Grain的单线程执行。一般来说,你应该不直接在grain代码中使用ConfigureAwait(false)

  4. 签名为async void的方法不应该与Grain一起使用。它们是为GUI事件处理程序准备的。async void方法如果允许一个异常逃逸,就会立即使当前进程崩溃,从而无法处理这个异常。对于List<T>.ForEach(async element => ...)和任何其他接受Action<T>的方法也是如此,因为异步委托将被强制变成一个async void委托。

Task.Factory.StartNew和async委托

通常建议,在任何C#程序中调度任务应使用Task.Run而不是Task.Factory.StartNew。 事实上,在快速谷歌一下Task.Factory.StartNew()的使用,会发现它很危险并且应该总是倾向于Task.Run。 但是,如果我们想保持谷物的单线程执行模式,我们就需要用到它,那么我们如何正确地使用它呢? 使用Task.Factory.StartNew()的风险在于,它并不支持原生的异步委托。 这意味着这可能是一个bug: var notIntendedTask = Task.Factory.StartNew(SomeDelegateAsync)notIntendedTask 不是 一个在SomeDelegateAsync完成时完成的任务。 相反,我们应该 总是 拆包返回的任务: var task = Task.Factory.StartNew(SomeDelegateAsync).Unwrap()

例子

下面的示例代码演示了TaskScheduler.CurrentTask.Run和一个自定义调度器的用法,以摆脱Orleans Grain上下文,以及如何返回到它。

  1. public async Task MyGrainMethod()
  2. {
  3. // Grab the grain's task scheduler
  4. var orleansTS = TaskScheduler.Current;
  5. await TaskDelay(10000);
  6. // Current task scheduler did not change, the code after await is still running
  7. // in the same task scheduler.
  8. Assert.AreEqual(orleansTS, TaskScheduler.Current);
  9. Task t1 = Task.Run( () =>
  10. {
  11. // This code runs on the thread pool scheduler, not on Orleans task scheduler
  12. Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
  13. Assert.AreEqual(TaskScheduler.Default, TaskScheduler.Current);
  14. });
  15. await t1;
  16. // We are back to the Orleans task scheduler.
  17. // Since await was executed in Orleans task scheduler context, we are now back
  18. // to that context.
  19. Assert.AreEqual(orleansTS, TaskScheduler.Current);
  20. // Example of using Task.Factory.StartNew with a custom scheduler to escape from
  21. // the Orleans scheduler
  22. Task t2 = Task.Factory.StartNew(() =>
  23. {
  24. // This code runs on the MyCustomSchedulerThatIWroteMyself scheduler, not on
  25. // the Orleans task scheduler
  26. Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
  27. Assert.AreEqual(MyCustomSchedulerThatIWroteMyself, TaskScheduler.Current);
  28. },
  29. CancellationToken.None,
  30. TaskCreationOptions.None,
  31. scheduler: MyCustomSchedulerThatIWroteMyself);
  32. await t2;
  33. // We are back to Orleans task scheduler.
  34. Assert.AreEqual(orleansTS, TaskScheduler.Current);
  35. }

例子 - 从运行在线程池上的代码中调用Grain

另一种情况是,一段Grain代码需要“脱离”Grain的任务调度模型,在线程池(或其他一些非Grain上下文)上运行,但仍然需要调用另一个Grain。 Grain调用可以从非Grain上下文中进行,无需额外的操作。

下面的代码演示了如何从运行在Grain内部但不在Grain上下文中的一段代码中进行Grain调用。

  1. public async Task MyGrainMethod()
  2. {
  3. // Grab the Orleans task scheduler
  4. var orleansTS = TaskScheduler.Current;
  5. var fooGrain = this.GrainFactory.GetGrain<IFooGrain>(0);
  6. Task<int> t1 = Task.Run(async () =>
  7. {
  8. // This code runs on the thread pool scheduler,
  9. // not on Orleans task scheduler
  10. Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
  11. int res = await fooGrain.MakeGrainCall();
  12. // This code continues on the thread pool scheduler,
  13. // not on the Orleans task scheduler
  14. Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
  15. return res;
  16. });
  17. int result = await t1;
  18. // We are back to the Orleans task scheduler.
  19. // Since await was executed in the Orleans task scheduler context,
  20. // we are now back to that context.
  21. Assert.AreEqual(orleansTS, TaskScheduler.Current);
  22. }

与其他库合作

你的代码使用的一些外部库可能在其内部使用了ConfigureAwait(false)。 事实上,在.NET中实现通用库时使用ConfigureAwait(false)是一种良好且正确的做法。 但这在Orleans中不是问题。 只要Grain中调用库方法的代码是用常规的await来等待库的调用,Grain代码就是正确的。 结果将与预期完全一致——库的代码将在默认调度器(由TaskScheduler.Default返回的值,这并不能保证计算续体一定会在ThreadPool线程上运行,因为计算续体经常被内联在前一个线程中)上运行,而Grain代码将在Grain的调度器上运行。

另一个常见问题是,是否需要用Task.Run来执行库的调用?也就是说,是否需要将库的代码显式转移(offload)到ThreadPool(对于Grain代码,Task.Run(() => myLibrary.FooAsync()))? 答案是否定的。 没有必要将任何代码转移到ThreadPool,除非是库的代码正在进行阻塞的同步调用。 通常情况下,任何写得好的、正确的.NET异步库(返回Task并以Async后缀命名的方法)都不会进行阻塞调用。 因此,除非你怀疑异步库有问题,或者你故意使用同步阻塞库,否则没有必要将任何东西转移到ThreadPool

死锁

由于Grain是单线程执行的,所以有可能通过同步阻塞来使一个Grain陷入死锁,其需要多个线程来解除阻塞。 这意味着,如果在调用方法或属性时,所提供的任务还没有完成,那么调用以下任何方法和属性的代码都会使Grain陷入死锁:

  • Task.Wait()
  • Task.Result
  • Task.WaitAny(...)
  • Task.WaitAll(...)
  • task.GetAwaiter().GetResult()

在任何高并发服务中都应该避免使用这些方法,它们会导致性能低下且不稳定,因为它们阻塞了可能正在进行有效工作的线程,并要求.NETThreadPool注入额外的线程以便完成这些工作,从而使.NETThreadPool陷入饥饿状态。 在执行Grain代码时,如上所述,这些方法会导致Grain出现死锁,因此在Grain代码中也应避免使用这些方法。

如果有一些无法避免的sync-over-async的工作,最好将这些工作放到一个单独的调度器中。 最简单的方法是以await Task.Run(() => task.Wait())为例。 请注意,我们强烈建议避免sync-over-async工作,因为如上所述,它将导致你的应用的可扩展性和性能受到影响。

总结:在Orleans中使用Task

你想做什么 如何实现
在.NET线程池的线程上运行后台工作。不允许有Grain代码或Grain调用。 Task.Run
在Grain代码中运行异步worker任务,并有Orleans的回合制并发保证(见上文)。 Task.Factory.StartNew(WorkerAsync).Unwrap()
在Grain代码中运行同步的worker任务,并有Orleans的回合制并发保证。 Task.Factory.StartNew(WorkerSync)
执行任务的超时 Task.Delay + Task.WhenAny
调用异步库方法 await此异步方法
使用 async/await 普通的 .NET Task-Async编程模型。受支持且推荐
ConfigureAwait(false) 不要在Grain代码内使用。只允许在库内使用。