grain激活体是单线程的,默认情况下,激活体会自始至终地处理完成每个请求后,才会处理下一个请求。在某些情况下,当一个请求等待异步操作完成时,对一个激活体来说,它可能需要处理其他请求。由于这个及其他的原因,Orleans为开发人员提供了对请求的交错行为的一些控制。在以下情况下,可以交错处理多个请求:
[Reentrant]
[AlwaysInterleave]
true
以下各节将讨论这些情况。
grain
的实现类可以用[Reentrant]
属性标记,以指示不同的请求可以自由地被交错。
换句话说,可重入的激活体,可以在上一个请求尚未完成处理的情况下,开始执行另一个请求。执行仍然限于单个线程,因此激活体仍然一次执行一个回合,并且每个回合仅代表激活体的一个请求执行。
可重入的grain代码永远不会并行运行多段grain代码(grain代码的执行将始终是单线程的),但是,可重入的grain可能会看到不同请求交错执行的代码。也就是说,来自不同请求的延续回合,是交错执行的。
例如,下面的伪代码,当Foo和Bar是同一个grain类的2个方法时:
Task Foo()
{
await task1; // line 1
return Do2(); // line 2
}
Task Bar()
{
await task2; // line 3
return Do2(); // line 4
}
如果这个grain被标记[Reentrant]
,则Foo和Bar的执行可能会交错。
例如,以下执行顺序是可能的:
第1行,第3行,第2行和第4行。即,来自不同请求的回合发生了交错。
如果grain不是可重入的,则唯一可能的执行是:第1行,第2行,第3行,第4行。或者:第3行,第4行,第1行,第2行(新请求无法在上一个完成之前开始)。
在选择grain可重入和不可重入时,主要的权衡是代码的复杂性(要使交错正确地工作),以及推理它的难度。
在一个微不足道的情况下,当grain是无状态,并且逻辑简单时,那么更少的可重入grain,通常会稍微高效一些(但不能太少,以便使用所有硬件线程)。
如果代码更复杂,大量的不可重入的grain,即使整体效率稍低一些,也会为您省去许多查找出不明显的交错问题时的痛苦。
最终的答案取决于应用程序的具体情况。
不管谷grain是否可重入,标记为[AlwaysInterleave]
的grain 接口的方法都将交错。请考虑以下示例:
public interface ISlowpokeGrain : IGrainWithIntegerKey
{
Task GoSlow();
[AlwaysInterleave]
Task GoFast();
}
public class SlowpokeGrain : Grain, ISlowpokeGrain
{
public async Task GoSlow()
{
await Task.Delay(TimeSpan.FromSeconds(10));
}
public async Task GoFast()
{
await Task.Delay(TimeSpan.FromSeconds(10));
}
}
现在考虑以下客户端请求启动的调用流程:
var slowpoke = client.GetGrain(0);
// A) This will take around 20 seconds
await Task.WhenAll(slowpoke.GoSlow(), slowpoke.GoSlow());
// B) This will take around 10 seconds.
await Task.WhenAll(slowpoke.GoFast(), slowpoke.GoFast(), slowpoke.GoFast());
呼叫GoSlow
不会交错,因此两次GoSlow()
呼叫的执行大约需要20秒。另一方面,因为GoFast
被标记[AlwaysInterleave]
,对它的三次调用将同时执行,并且将在大约10秒内完成,而不需要至少30秒完成。
为了避免死锁,调度程序允许在给定的调用链中重入。考虑以下两个具有相互递归方法的grain的例子,即IsEven
和IsOdd
:
public interface IEvenGrain : IGrainWithIntegerKey
{
Task<bool> IsEven(int num);
}
public interface IOddGrain : IGrainWithIntegerKey
{
Task<bool> IsOdd(int num);
}
public class EvenGrain : Grain, IEvenGrain
{
public async Task<bool> IsEven(int num)
{
if (num == 0) return true;
var oddGrain = this.GrainFactory.GetGrain(0);
return await oddGrain.IsOdd(num - 1);
}
}
public class OddGrain : Grain, IOddGrain
{
public async Task<bool> IsOdd(int num)
{
if (num == 0) return false;
var evenGrain = this.GrainFactory.GetGrain(0);
return await evenGrain.IsEven(num - 1);
}
}
现在考虑以下客户端请求启动的调用流程:
var evenGrain = client.GetGrain(0);
await evenGrain.IsEven(2);
上面的代码调用IEvenGrain.IsEven(2)
,IsEven(2)
又调用IOddGrain.IsOdd(1)
,然后IsOdd(1)又调用
IEvenGrain.IsEven(0)
,而IsEven(0)返回true
,通过调用链最终返回给客户端。如果没有调用链重入,当IOddGrain
调用IEvenGrain.IsEven(0)
时,上面的代码将导致死锁。然而,通过调用链重入,允许调用继续进行,因为它被认为是开发者的意图。
通过将SchedulingOptions.AllowCallChainReentrancy
设置false
,可以禁用此行为。例如:
siloHostBuilder.Configure(
options => options.AllowCallChainReentrancy = false);
grain类可以通过检查请求来指定一个谓词,此谓词用于在挨个调用的基础上确定交错。[MayInterleave(string methodName)]
属性提供此功能。该属性的参数是grain类中静态方法的名称,该方法接受一个InvokeMethodRequest
对象并返回一个bool
,指示请求是否应该交错。
下面是一个示例,如果请求的参数类型具有[Interleave]
属性,则允许交错:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public sealed class InterleaveAttribute : Attribute { }
// Specify the may-interleave predicate.
[MayInterleave(nameof(ArgHasInterleaveAttribute))]
public class MyGrain : Grain, IMyGrain
{
public static bool ArgHasInterleaveAttribute(InvokeMethodRequest req)
{
// Returning true indicates that this call should be interleaved with other calls.
// Returning false indicates the opposite.
return req.Arguments.Length == 1
&& req.Arguments[0]?.GetType().GetCustomAttribute() != null;
}
public Task Process(object payload)
{
// Process the object.
}
}