Plánování žádostí
Aktivace zrnitosti mají model provádění s jedním vláknem a ve výchozím nastavení zpracují každý požadavek od začátku do dokončení, než může další požadavek začít zpracovávat. 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 dává vývojáři určitou kontrolu nad chováním prokládání požadavků, jak je popsáno v části Reentrancy . Následuje příklad nesdělování plánování požadavků, což je výchozí chování .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. - Protokoly
"2"
a vrátí se zpět do původního volajícího.
Zatímco A čeká na volání na B, nemůže zpracovat žádné příchozí žádosti. V důsledku toho, pokud se A a B vzájemně volali současně, mohou při čekání na dokončení těchto volání zablokovat . Tady je příklad založený na klientovi, který 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 zpět 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. - Volání zpracuje
Ping()
a vrátí se do buňky B, která se vrátí 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ě. - Protokol zrn
"1"
a pokračujteawait other.Ping()
. - Vzhledem k tomu, že obě zrnka jsou stále zaneprázdněná (zpracování
CallOther
žádosti, která ještě nebyla dokončena),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 bubliny až do původního volajícího.
Následující část popisuje, jak zabránit vzájemnému zablokování tím, že umožňuje více požadavků prokládání jejich provádění mezi sebou.
Vícenásobný přístup
Orleans výchozí nastavení pro výběr bezpečného toku spuštění: jeden, ve kterém se vnitřní stav agregace během více požadavků současně nezmě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á náklady, které byly dříve popsány, především liveness: určité vzorce volání můžou vést k zablokování. Jedním ze způsobů, jak se vyhnout vzájemným zablokováním, je zajistit, aby volání odstupňovaného intervalu nikdy neproběhne cyklus. Často je obtížné napsat kód, který je bez cyklu a nemůže se zablokovat. Č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 agregační metody provádí některé asynchronní požadavky na databázovou službu, pak agregační interval pozastaví provádění požadavku, dokud odpověď z databáze nedorazí do agregačního intervalu.
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, které umožňují souběžné spouštění některých nebo všech požadavků, prokládání jejich provádění mezi sebou. Tyto Orleansobavy se v této oblasti označují jako reentrancy nebo prokládání. 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í.
V následujících případech může být prokládání více požadavků:
- Třída zrnitosti je označena značkou ReentrantAttribute.
- Metoda rozhraní je označena AlwaysInterleaveAttribute.
- Predikát agregace MayInterleaveAttribute vrátí
true
.
Při opětovném vytvoření se následující případ stane platným spuštěním a možnost předchozího zablokování se odebere.
Případ 3: Odstupňované nebo metody se znovu propojí
V tomto příkladu se zrnka A a B můžou vzájemně volat současně, aniž by to mohlo mít potenciál pro plánování zablokování požadavků, protože obě zrnka jsou znovu účastníky. V následujících částech najdete další podrobnosti o opětovném zobrazení.
Opětovně nasoukaná zrnka
Třídy Grain implementace mohou být označeny ReentrantAttribute k označení, že různé požadavky mohou být volně prokládání.
Jinými slovy, aktivace znovu účastníky 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 omezené na jedno vlákno, takže aktivace stále probíhá po jednom turnu a každé z nich se spouští jménem pouze jednoho z požadavků aktivace.
Kód zrnitého opakování nikdy nespustí paralelně více částí kódu zrnitosti (provádění kódu zrn je vždy jednovláknové), ale opětovné zrna může vidět provádění kódu pro různé požadavky prokládání. To znamená, že pokračování se mění z různých požadavků může prokládání.
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
může prokládání.
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 mění od různých požadavků prokládání.
Pokud by se znovu nenasadilo, jediným možnými provedeními by bylo: řádek 1, řádek 2, řádek 3, řádek 4 NEBO: řádek 3, řádek 4, řádek 1, řádek 2 (nový požadavek nemůže začínat před předchozím dokončením).
Hlavním kompromisem při volbě mezi reentrantem a nonreentrantní zrní je kód složitosti prokládání práce správně, a potíže s odůvodněním.
V triviálním případě, kdy jsou zrnka bezstavová a logika je jednoduchá, méně (ale není příliš málo, takže se používají všechna hardwarová vlákna) by měla být obecně o něco efektivnější.
Pokud je kód složitější, pak větší počet nerekentrantních zrn, i když mírně méně efektivní celkově, by vám měl ušetřit mnoho smutku při zjišťování neposlušných prokládání problémů.
Odpověď nakonec závisí na specifikách aplikace.
Metody prokládání
Metody podrobného rozhraní označené metodou AlwaysInterleaveAttribute, vždy prokládání jakéhokoli jiného požadavku a mohou být vždy prokládání s jakýmkoli jiným požadavkem, a to i požadavky na metody, které nejsou-[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í, která GoSlow
nejsou prokládání, takže celková doba provádění těchto dvou GoSlow
volání trvá přibližně 20 sekund. Na druhou stranu GoFast
je označena AlwaysInterleaveAttributea tři volání se provádí souběžně, takže se dokončí přibližně za 10 sekund, místo aby bylo nutné dokončit alespoň 30 sekund.
Metody jen pro čtení
Pokud metoda zrnitosti nezmění stav agregačního intervalu, je bezpečné provést souběžně s jinými požadavky. Označuje ReadOnlyAttribute , že metoda nemění stav agregace. 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();
}
Opětovné zařazení řetězu volání
Pokud zrno volá metodu, která v jiném zrnu, která pak volá zpět do původního zrna, volání způsobí zablokování, pokud se volání znovu nezapojí. Přeentrování je možné povolit pro jednotlivé volání na základě volání pomocí přeentrancy zřetězení volání. Chcete-li povolit opětovné volání řetězu volání, zavolejte metodu AllowCallChainReentrancy() , která vrátí hodnotu, která umožňuje znovu zadat z jakéhokoli volajícího dále dolů řetěz volání, dokud nebude uvolněn. To zahrnuje opětovné provázání z agregační 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)
se volání do ChatRoomGrain.OnJoinRoom(user)
, který se pokusí zavolat zpět UserGrain.GetDisplayName()
na získání zobrazovaného jména uživatele. 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 roomGrain
pouze volání zpět do UserGrain
. To vám umožní jemně odstupňovanou kontrolu nad tím, kde a jak je povolena opětovná podpora.
Pokud byste místo toho zabránili vzájemnému zablokování anotací GetDisplayName()
deklarace metody s IUserGrain
[AlwaysInterleave]
, umožnili byste jakékoli podrobné prokládání volání s jakoukoli jinou metodou GetDisplayName
. Místo toho můžete volat pouze roomGrain
metody v našem zrnití a pouze do doby, než scope
se odstraní.
Potlačení opětovného zařazení ř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é, aby knihovny, které rozšiřují Orleans odstupňované funkce, jako jsou streamovací a všesměrové kanály, aby vývojáři zachovali plnou kontrolu nad tím, kdy je povolené opětovné zařazení řetězu volání.
Metoda GetCount
nemění stav zrnitosti, takže je označena ReadOnly
. Volající čekající na vyvolání této metody nejsou blokovány jinými ReadOnly
požadavky na agregaci a metoda se vrátí okamžitě.
Přejmezení pomocí predikátu
Třídy zrnitosti mohou určit predikát k určení prokládání na volání na základě 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
indikující, zda má být požadavek prokládání.
Tady je příklad, který umožňuje prokládání, pokud typ argumentu [Interleave]
požadavku má atribut:
[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.
}
}