Plánování žádostí
Aktivace zrn mají jednovláknový model provádění a ve výchozím nastavení zpracovávají každý požadavek od začátku do konce, než může začít zpracování dalšího požadavku. V některých případech může být žádoucí, aby aktivace zpracovávala jiné požadavky, zatímco jeden požadavek čeká na dokončení asynchronní operace. Z tohoto a jiných důvodů Orleans poskytuje vývojáři určitou míru kontroly nad chováním prokládání požadavků, jak je podrobně popsáno v části „Reentrancy“. Následuje příklad nereentrantního plánování požadavků, což je výchozí chování v Orleans.
Zvažte následující PingGrain
definici:
public interface IPingGrain : IGrainWithStringKey
{
Task Ping();
Task CallOther(IPingGrain other);
}
public class PingGrain : Grain, IPingGrain
{
private readonly ILogger<PingGrain> _logger;
public PingGrain(ILogger<PingGrain> logger) => _logger = logger;
public Task Ping() => Task.CompletedTask;
public async Task CallOther(IPingGrain other)
{
_logger.LogInformation("1");
await other.Ping();
_logger.LogInformation("2");
}
}
V našem příkladu jsou zapojeny dvě zrnka typu PingGrain
A a B. Volající vyvolá následující volání:
var a = grainFactory.GetGrain("A");
var b = grainFactory.GetGrain("B");
await a.CallOther(b);
Tok provádění je následující:
- Hovor dorazí do A, který protokoluje
"1"
a pak vydá hovor do B. -
B se vrátí okamžitě zezadu
Ping()
do A. -
Ona se přihlásí
"2"
a vrátí se zpět k původnímu volajícímu.
Zatímco A čeká na volání na B, nemůže zpracovat žádné příchozí žádosti. Pokud by se A a B vzájemně volali současně, mohlo by to v důsledku vést k zablokování při čekání na dokončení těchto hovorů. Tady je příklad založený na tom, že klient vydává následující volání:
var a = grainFactory.GetGrain("A");
var b = grainFactory.GetGrain("B");
// A calls B at the same time as B calls A.
// This might deadlock, depending on the non-deterministic timing of events.
await Task.WhenAll(a.CallOther(b), b.CallOther(a));
Případ 1: Volání nemají vzájemné zablokování
V tomto příkladu:
- Hovor
Ping()
od A dorazí na B před příchodemCallOther(a)
hovoru na B. -
Proto B zpracuje
Ping()
volání před volánímCallOther(a)
. - Vzhledem k tomu, že B zpracovává
Ping()
volání, A se může vrátit volajícímu. - Když B vydá
Ping()
volání do A, A je stále zaneprázdněn protokolováním zprávy ("2"
), takže hovor musí čekat na krátkou dobu, ale brzy bude možné ji zpracovat. -
A zpracovává
Ping()
volání a vrací se k B, která se vrací původnímu volajícímu.
Zvažte méně šťastnou řadu událostí: jednu, ve které stejný kód vede k zablokování kvůli mírně odlišnému načasování.
Případ 2: vzájemné zablokování volání
V tomto příkladu:
- Volání
CallOther
přicházejí na příslušná zrna a zpracovávají se současně. - Oba protokoly zrn
"1"
a pokračujteawait other.Ping()
. - Protože oba procesy jsou stále zaneprázdněné (protože zpracovávají
CallOther
žádosti, které ještě nejsou dokončeny),Ping()
požadavky čekají. - Po určité době určí, Orleans že vypršel časový limit volání, a každé
Ping()
volání způsobí vyvolání výjimky. - Tělo
CallOther
metody nezpracuje výjimku a přenese se až k původnímu volajícímu.
Následující část popisuje, jak zabránit vzájemnému zablokování umožněním více požadavkům prokládat jejich provádění navzájem.
Znovuvstupnost
Orleans automaticky vybírá bezpečný tok provádění: takový, při kterém není vnitřní stav zrna během více požadavků současně změněn. Souběžné úpravy interního stavu komplikují logiku a zatěžují vývojáře. Tato ochrana proti těmto druhům chyb souběžnosti má svou cenu, jak bylo dříve uvedeno, především liveness: určité způsoby volání mohou vést k zablokování. Jedním ze způsobů, jak se vyhnout vzájemným zablokováním, je zajistit, aby zrnková volání nikdy nezpůsobila cyklus. Často je obtížné napsat kód, který neobsahuje cykly a nezasekne se. Čekání na spuštění každého požadavku od začátku do dokončení před zpracováním dalšího požadavku může také poškodit výkon. Pokud například metoda grainu provádí nějaký asynchronní požadavek na databázovou službu, pak grain pozastaví provádění požadavku, dokud nedorazí odpověď z databáze.
Každý z těchto případů je popsán v následujících částech. Z těchto důvodů Orleans poskytuje vývojářům možnosti ke spouštění některých nebo všech požadavků souběžně, přičemž mohou být jejich provádění prokládána mezi sebou. Tyto obavy se označují jako reentrancy nebo prokládání v Orleans. Prováděním požadavků současně můžou zrnka, která provádějí asynchronní operace, zpracovávat více požadavků v kratším období.
Ve následujících případech mohou být proloženy více požadavků:
- Třída zrnitosti je označena značkou ReentrantAttribute.
- Metoda rozhraní je označena AlwaysInterleaveAttribute.
- Predikát zrna MayInterleaveAttribute vrátí
true
.
Při znovuvstupu se následující případ stává platným vykonáním a možnost výše uvedeného zablokování se eliminuje.
Případ 3: struktura nebo metoda je reentrantní
V tomto příkladu se vlákna A a B mohou vzájemně volat současně, aniž by hrozilo zablokování při plánování požadavků, protože obě vlákna jsou znovu vstupující. V následujících částech najdete další podrobnosti o reentrantnosti.
Zpětně vstupující zrna
Třídy Grain implementace mohou být označeny ReentrantAttribute k určení, že různé požadavky mohou být bez omezení prokládány.
Jinými slovy, opětovná aktivace může začít spouštět další žádost, zatímco předchozí žádost ještě nedokončila zpracování. Provádění je stále omezeno na jedno vlákno, takže aktivace stále probíhá po jednom úkonu, a každý úkon je prováděn pouze na základě jednoho z požadavků aktivace.
Znovuvstupný kód zrna nikdy nespouští paralelně více částí zrnitého kódu (provádění kódu zrna je vždy jednovláknové), ale znovuvstupné zrno může zaznamenat, že se provádění kódu pro různé požadavky prolíná. To znamená, že pokračování různých požadavků může být prokládáno.
Například, jak je znázorněno v následujícím pseudokódu, vezměte v úvahu, že Foo
a Bar
jsou dvě metody stejné třídy zrnitosti:
Task Foo()
{
await task1; // line 1
return Do2(); // line 2
}
Task Bar()
{
await task2; // line 3
return Do2(); // line 4
}
Pokud je toto zrno označeno ReentrantAttribute, provádění Foo
a Bar
se může prokládat.
Například následující pořadí provádění je možné:
Řádek 1, řádek 3, řádek 2 a řádek 4. To znamená, že se střídají úseky z různých požadavků.
Pokud by kód nebyl reentrantní, jedinými možnými provedeními by byla: řádek 1, řádek 2, řádek 3, řádek 4 NEBO: řádek 3, řádek 4, řádek 1, řádek 2 (nový požadavek nemůže začít, dokud předchozí není dokončen).
Hlavním kompromisem při volbě mezi reentrantními a nereetrantními zrny je složitost kódu pro správné fungování prokládání a obtížnost jeho pochopení.
V triviálním případě, kdy jsou jednotky bezstavové a logika je jednoduchá, by méně znovu-začínajících jednotek (ale ne příliš málo, aby se využila všechna hardwarová vlákna) mělo být obecně o něco účinnější.
Pokud je kód složitější, pak větší počet nerekurantních částí, i když celkově o něco méně efektivní, by vám měl ušetřit mnoho problémů při zjišťování nenápadných prokládacích problémů.
Odpověď nakonec závisí na specifikách aplikace.
Metody prokládání
Metody rozhraní grain označené AlwaysInterleaveAttribute vždy prokládají jakýkoli jiný požadavek a mohou být vždy prokládány s jakýmkoli jiným požadavkem, i požadavky na metody, které nemají atribut [AlwaysInterleave].
Představte si následující příklad:
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));
}
}
Zvažte tok volání iniciovaný následujícím požadavkem klienta:
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());
Volání na GoSlow
nejsou prokládána, takže celková doba provádění obou volání GoSlow
trvá přibližně 20 sekund. Na druhou stranu je GoFast
označen AlwaysInterleaveAttribute, a tři hovory k němu se provádí souběžně, takže se dokončí přibližně za 10 sekund, místo aby to trvalo alespoň 30 sekund.
Metody pouze pro čtení
Pokud metoda práce se zrnem nezmění stav zrna, je bezpečné provádět ji souběžně s jinými požadavky.
ReadOnlyAttribute ukazuje, že metoda nemění stav grainu. Označení metod, jak ReadOnly
umožňuje Orleans zpracovávat požadavek souběžně s jinými ReadOnly
požadavky, což může výrazně zlepšit výkon vaší aplikace. Představte si následující příklad:
public interface IMyGrain : IGrainWithIntegerKey
{
Task<int> IncrementCount(int incrementBy);
[ReadOnly]
Task<int> GetCount();
}
Metoda GetCount
nemění stav zrnitosti, takže je označena ReadOnly
. Volající, kteří čekají na vyvolání této metody, nejsou blokováni jinými ReadOnly
požadavky na grain a metoda se okamžitě vrátí.
Reentrantnost řetězce volání
Pokud zrno volá metodu v jiném zrnu, které pak volá zpět do původního zrna, volání způsobí zablokování, pokud volání není znovu-vstupné. Znovuvstup lze povolit pro jednotlivá místa volání pomocí znovuvstupu zřetězení volání. Chcete-li povolit znovuotevření řetězce volání, zavolejte metodu AllowCallChainReentrancy(), která vrátí hodnotu umožňující opakované volání z jakéhokoli volajícího dále po řetězci volání, dokud nedojde k jeho uvolnění. To zahrnuje opětovný vstup z procesu volání samotné metody. Představte si následující příklad:
public interface IChatRoomGrain : IGrainWithStringKey
{
ValueTask OnJoinRoom(IUserGrain user);
}
public interface IUserGrain : IGrainWithStringKey
{
ValueTask JoinRoom(string roomName);
ValueTask<string> GetDisplayName();
}
public class ChatRoomGrain : Grain<List<(string DisplayName, IUserGrain User)>>, IChatRoomGrain
{
public async ValueTask OnJoinRoom(IUserGrain user)
{
var displayName = await user.GetDisplayName();
State.Add((displayName, user));
await WriteStateAsync();
}
}
public class UserGrain : Grain, IUserGrain
{
public ValueTask<string> GetDisplayName() => new(this.GetPrimaryKeyString());
public async ValueTask JoinRoom(string roomName)
{
// This prevents the call below from triggering a deadlock.
using var scope = RequestContext.AllowCallChainReentrancy();
var roomGrain = GrainFactory.GetGrain<IChatRoomGrain>(roomName);
await roomGrain.OnJoinRoom(this.AsReference<IUserGrain>());
}
}
V předchozím příkladu UserGrain.JoinRoom(roomName)
volá do ChatRoomGrain.OnJoinRoom(user)
, který se pokouší zpětně volat do UserGrain.GetDisplayName()
pro získání uživatelského zobrazovaného jména. Vzhledem k tomu, že tento řetěz volání zahrnuje cyklus, výsledkem bude zablokování, pokud UserGrain
nepovolí opětovné provázání pomocí některého z podporovaných mechanismů probíraných v tomto článku. V tomto případě používáme AllowCallChainReentrancy(), který umožňuje, aby pouze roomGrain
mohl volat zpět do UserGrain
. To vám umožní podrobnou kontrolu nad tím, kde a jak je povolena reentrantnost.
Pokud byste místo toho zabránili vzájemnému zablokování tím, že anotujete deklaraci metody GetDisplayName()
na IUserGrain
pomocí [AlwaysInterleave]
, umožnili byste proložení volání GetDisplayName
s jakoukoli jinou metodou. Místo toho můžete volat pouzeroomGrain
metody na našem grainu a pouze do doby, než scope
bude odstraněn.
Potlačení opakovaného vstupu do řetězu volání
Opětovné volání řetězu volání lze také potlačovat pomocí SuppressCallChainReentrancy() metody. To má omezenou užitečnost pro koncové vývojáře, ale je důležité pro vnitřní použití knihovnami, které rozšiřují funkcionalitu Orleans, jako je streamování a vysílací kanály, aby vývojáři zachovali plnou kontrolu nad tím, kdy je povolena opětovná vstupnost do řetězu volání.
Reentrantnost pomocí predikátu
Třídy grainů mohou určit predikát pro určení prokládání u jednotlivých volání kontrolou požadavku. Atribut [MayInterleave(string methodName)]
poskytuje tuto funkci. Argumentem atributu je název statické metody v rámci třídy 'grain', která přijímá InvokeMethodRequest objekt a vrací bool
, který indikuje, zda má být požadavek prokládán.
Tady je příklad, který umožňuje prokládání, pokud má typ argumentu požadavku atribut [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(IInvokable 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.
}
}