Orleans 2.0官方文件(閆輝的個人翻譯)——4.6 重入
重入
grain啟用體是單執行緒的,預設情況下,啟用體會自始至終地處理完成每個請求後,才會處理下一個請求。在某些情況下,當一個請求等待非同步操作完成時,對一個啟用體來說,它可能需要處理其他請求。由於這個及其他的原因,Orleans為開發人員提供了對請求的交錯行為的一些控制。在以下情況下,可以交錯處理多個請求:
- grain類標記為
[Reentrant]
- 介面方法標記為
[AlwaysInterleave]
- 同一個呼叫鏈中的請求
- grain的MayInterleave謂詞返回
true
以下各節將討論這些情況。
可重入的grain
grain
[Reentrant]
屬性標記,以指示不同的請求可以自由地被交錯。
換句話說,可重入的啟用體,可以在上一個請求尚未完成處理的情況下,開始執行另一個請求。執行仍然限於單個執行緒,因此啟用體仍然一次執行一個回合,並且每個回合僅代表啟用體的一個請求執行。
可重入的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<ISlowpokeGrain>(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<IOddGrain>(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<IEvenGrain>(0);
return await evenGrain.IsEven(num - 1);
}
}
現在考慮以下客戶端請求啟動的呼叫流程:
var evenGrain = client.GetGrain<IEvenGrain>(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<SchedulingOptions>(
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<InterleaveAttribute>() != null;
}
public Task Process(object payload)
{
// Process the object.
}
}