根据设计,从grain代码中生成的任何子任务(例如,通过使用await
或ContinueWith
或Task.Factory.StartNew
),将在与父任务相同的每个激活的TPL Task Sheduler上调度,因此继承了与grain代码的其余部分相同的单线程执行模型。这是基于grain回合制的并发性背后的要点。
在某些情况下,grain代码可能需要“突破”Orleans任务调度模型,和“做一些特殊的事情”,例如显式地将一个Task
指向不同的任务调度程序或使用.NET线程池。这种情况的一个例子是,当grain代码必须执行同步远程阻塞调用(如远程IO)时。在grain环境中执行此操作,将阻塞grain以及Orleans线程之一,因此永远不应该这样做。相反,grain代码可以在线程池的线程上,执行这段阻塞代码,并且join(await
)该执行的完成,然后在grain上下文中继续。我们预计,从Orleans调度程序中脱离出来,将是一种非常高级且很少需要的使用场景,超出“正常”的使用模式。
1) await
,Task.Factory.StartNew
,Task.ContinuewWith
,Task.WhenAny
,Task.WhenAll
,Task.Delay
都遵循当前的任务计划。这意味着以默认方式使用它们,无需传递不同的TaskScheduler,它们会在grain上下文中执行。
2)Task.Factory.FromAsync
的Task.Run
和endMethod
委托,都不遵循当前的任务调度程序。它们都使用TaskScheduler.Default
调度程序,即.NET线程池任务调度程序。因此,Task.Run
和endMethod
中的代码,将始终运行在Orleans grain单线程执行模型外部的.NET线程池中运行,如详述。但是,await Task.Run
或await Task.Factory.FromAsync
之后的任何代码,在创建任务后,会回到调度程序下运行,即grain调度程序。
3)configureAwait(false)
是一个显式API,用来脱离当前任务Scheduler。它将导致在等待的Task之后的代码,在TaskScheduler.Default
调度程序(即.NET线程池)上执行,从而打破了Orleans grain的单线程执行模型。一般而言,您应该永远不要在grain代码中,直接使用configureAwait(false)
。
4)带有async void
签名的方法,不应与grain一起使用。它们适用于图形用户界面事件处理程序。
下面是示例代码,演示了如何使用TaskScheduler.Current、
Task.Run
以及一个特殊的自定义调度程序,以脱离Orlean grain上下文,以及如何返回到grain上下文。
public async Task MyGrainMethod()
{
// Grab the Orleans task scheduler
var orleansTs = TaskScheduler.Current;
await TaskDelay(10000);
// 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 ask.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的代码需要“突破”Orleans任务调度模型,并在线程池(或其他一些非Orleans上下文)上运行,但它仍然需要调用另一个grain。如果您尝试进行grain调用,但不在Orleans上下文中,则会收到一个异常,表示您“尝试在silo上发送消息,既不是从grain内发送消息,也不是在系统目标内发送消息(RuntimeContext不是设置为SchedulingContext)“。
下面的代码演示了如何从一段代码中进行grain调用,该代码在grain内部运行,但不在grain上下文中运行。
public async Task MyGrainMethod()
{
// Grab the Orleans task scheduler
var orleansTs = TaskScheduler.Current;
Task<int> t1 = Task.Run(async () =>
{
// This code runs on the thread pool scheduler, not on Orleans task scheduler
Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
// You can do whatever you need to do here. Now let's say you need to make a grain call.
Task<Task<int>> t2 = Task.Factory.StartNew(() =>
{
// This code runs on the Orleans task scheduler since we specified the scheduler: orleansTs.
Assert.AreEqual(orleansTS, TaskScheduler.Current);
return GrainFactory.GetGrain<IFooGrain>(0).MakeGrainCall();
}, CancellationToken.None, TaskCreationOptions.None, scheduler: orleansTs);
int res = await (await t2); // double await, unrelated to Orleans, just part of TPL APIs.
// This code runs back 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不是问题。只要调用库方法的grain代码,以常规await
的方式,等待库的调用,那么grain代码就是正确的。结果将完全符合预期——库代码将在Default调度程序上运行后续代码(这恰好是ThreadPoolTaskScheduler
,但它不能保证后续代码绝对会在ThreadPool的线程上运行,因为后续代码通常在前一个线程中是内联的) ,而grain代码将在Orleans的调度程序上运行。
另一个常见问题是,是否需要使用Task.Run
执行库调用——也就是说,是否需要将库代码显式转卸到ThreadPool(用于执行谷物代码Task.Run(()=> myLibrary.FooAsync())
)。答案是否定的。除了正在进行阻塞同步调用的库代码外,不需要将任何代码转卸到ThreadPool。通常,任何编写良好且正确的.NET异步库(返回Task
并以Async
后缀命名的方法)都不会进行阻塞调用。因此,除非您怀疑异步库是错误的或者有意使用同步阻塞库,否则无需将任何内容转卸到ThreadPool。
你想做什么? | 怎么做 |
---|---|
在.NET线程池线程上运行后台工作。无grain代码或grain调用。 | Task.Run |
grain 接口调用 | 方法返回类型为 Task 或Task<T> |
使用Orleans回合制并发保证,从grain代码运行工作任务。 | Task.Factory.StartNew |
执行工作项的超时时间 | Task.Delay + Task.WhenAny |
使用async /await | 正常的.NET任务 - 异步编程模型。支持和推荐 |
ConfigureAwait(false) | 不要在grain内部使用。仅允许在库内部。 |
调用异步库 | await 库调用 |