Aanvraagplanning
Grainactiveringen hebben een uitvoeringsmodel met één thread en verwerken standaard elke aanvraag volledig van begin tot einde voordat de volgende aanvraag kan worden verwerkt. In sommige gevallen kan het wenselijk zijn voor activering om andere aanvragen te verwerken terwijl één aanvraag wacht tot een asynchrone bewerking is voltooid. Om deze en andere redenen geeft Orleans de ontwikkelaar enige controle over het interleaving-gedrag van de aanvraag, zoals beschreven in de sectie Reentrancy. Hieronder volgt een voorbeeld van niet-reentrant-aanvraagplanning. Dit is het standaardgedrag in Orleans.
Houd rekening met de volgende PingGrain
definitie:
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");
}
}
In ons voorbeeld zijn twee soorten korrels betrokken: PingGrain
en : A en B. Een beller doet de volgende aanroep:
var a = grainFactory.GetGrain("A");
var b = grainFactory.GetGrain("B");
await a.CallOther(b);
De uitvoeringsstroom is als volgt:
- De oproep komt aan bij A, dat registreert
"1"
en vervolgens een aanroep naar B doet. -
B keert onmiddellijk terug van
Ping()
terug naar A. -
A logt
"2"
en gaat terug naar de oorspronkelijke aanroeper.
Terwijl A wacht op de aanroep naar B, kunnen er geen binnenkomende aanvragen worden verwerkt. Als A en B elkaar dus tegelijkertijd zouden aanroepen, kunnen ze een impasse vormen terwijl ze wachten tot deze oproepen zijn voltooid. Hier volgt een voorbeeld, op basis van de client die de volgende aanroep uitgeeft:
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));
Case 1: de oproepen zijn niet vastgelopen
In dit voorbeeld:
- De
Ping()
oproep van A arriveert bij B voordat deCallOther(a)
oproep bij B aankomt. -
Daarom verwerkt B de
Ping()
aanroep vóór deCallOther(a)
aanroep. - Omdat B de
Ping()
aanroep verwerkt, kan A teruggaan naar de beller. - Wanneer B de
Ping()
aanroep naar A uitgeeft, is A nog bezig met het vastleggen van het bericht ("2"
), dus de oproep moet een korte duur wachten, maar deze kan binnenkort worden verwerkt. -
Een verwerkt de
Ping()
aanroep en keert terug naar B, die terugkeert naar de oorspronkelijke beller.
Overweeg een minder gelukkige reeks gebeurtenissen: een waarbij dezelfde code resulteert in een deadlock vanwege iets andere timing.
Case 2: de oproep impasse
In dit voorbeeld:
- De
CallOther
aanroepen komen aan bij hun respectieve korrels en worden tegelijkertijd verwerkt. - Beide graanlogboeken in
"1"
en ga verder naarawait other.Ping()
. - Omdat beide korrels nog bezig zijn met het verwerken van de
CallOther
aanvraag, welke nog niet is voltooid, wachten dePing()
aanvragen. - Na een tijdje bepaalt Orleans dat de oproep een time-out heeft en dat elke
Ping()
oproep resulteert in een uitzondering die wordt opgeworpen. - Het
CallOther
methodelichaam verwerkt de uitzondering niet en het werkt door tot de oorspronkelijke aanroeper.
In de volgende sectie wordt beschreven hoe u impasses kunt voorkomen door meerdere aanvragen toe te staan om hun uitvoering met elkaar te interleaveen.
Herintreding
Orleans standaard een veilige uitvoeringsstroom kiezen: een stroom waarin de interne status van een graan niet gelijktijdig wordt gewijzigd tijdens meerdere aanvragen. Gelijktijdige aanpassing van de interne status bemoeilijkt logica en brengt de ontwikkelaar een grotere last. Deze bescherming tegen dergelijke gelijktijdigheidsfouten heeft een prijs, die eerder werd besproken, voornamelijk liveness: bepaalde oproeppatronen kunnen leiden tot deadlocks. Een manier om impasses te voorkomen, is ervoor te zorgen dat graanoproepen nooit resulteren in een cyclus. Vaak is het moeilijk om code te schrijven die cyclusvrij is en die niet kan vastlopen. Als u wacht totdat elke aanvraag van begin tot voltooiing wordt uitgevoerd voordat de volgende aanvraag wordt verwerkt, kan dit ook de prestaties schaden. Als een graanmethode bijvoorbeeld standaard een asynchrone aanvraag naar een databaseservice uitvoert, wordt de uitvoering van de aanvraag onderbroken totdat het antwoord van de database bij het graan aankomt.
Elk van deze gevallen wordt besproken in de volgende secties. Om deze redenen biedt Orleans ontwikkelaars opties waarmee sommige of alle aanvragen gelijktijdig kunnen worden uitgevoerd, waarbij de uitvoering wordt afgewisseld. In Orleans worden dergelijke zorgen ook wel aangeduid als reentrancy of interleaving. Door aanvragen gelijktijdig uit te voeren, kunnen entiteiten die asynchrone bewerkingen uitvoeren meer aanvragen in een kortere tijdspanne verwerken.
In de volgende gevallen kunnen meerdere aanvragen worden verwerkt:
- De graanklasse is gemarkeerd met ReentrantAttribute.
- De interfacemethode is gemarkeerd met AlwaysInterleaveAttribute.
- Het predicaat van het graan MayInterleaveAttribute retourneert
true
.
Met reentrancy wordt het volgende geval een geldige uitvoering en wordt de kans op de hierboven beschreven impasse weggehaald.
Case 3: het graan of de methode is opnieuw aan het reentrant
In dit voorbeeld kunnen grains A en B elkaar tegelijkertijd aanroepen zonder het risico op vergrendeling bij het plannen van aanvragen, omdat beide grains herintredend zijn. In de volgende secties vindt u meer informatie over reentrancy.
Terugkerende korrels
De Grain implementatieklassen kunnen worden gemarkeerd met de ReentrantAttribute om aan te geven dat verschillende aanvragen vrij kunnen worden doorgegeven.
Met andere woorden, een nieuwe activering kan beginnen met het uitvoeren van een andere aanvraag terwijl een eerdere aanvraag nog niet is verwerkt. De uitvoering is nog steeds beperkt tot één thread, dus de activering wordt nog steeds één keer uitgevoerd en elke beurt wordt uitgevoerd namens slechts één van de aanvragen van de activering.
Re-entrant graancode voert nooit meerdere stukken graancode parallel uit (de uitvoering van graancode is altijd enkelvoudige threading), maar re-entrant grains kunnen de uitvoering van code voor verschillende aanvragen zien afwisselen. Dat wil zeggen dat de voortzettingen van verschillende aanvragen elkaar kunnen overlappen.
Denk bijvoorbeeld aan twee methoden van dezelfde graanklasse, zoals wordt weergegeven in de volgende pseudocode Foo
Bar
:
Task Foo()
{
await task1; // line 1
return Do2(); // line 2
}
Task Bar()
{
await task2; // line 3
return Do2(); // line 4
}
Als dit graan is gemarkeerd, kan de uitvoering van ReentrantAttribute en Foo
interleave worden uitgevoerdBar
.
De volgende uitvoeringsvolgorde is bijvoorbeeld mogelijk:
Regel 1, regel 3, regel 2 en regel 4. Dat wil zeggen, de beurten van verschillende aanvragen verstrengelen.
Als de korrel niet re-entrant was, zouden de enige mogelijke uitvoeringen zijn: regel 1, regel 2, regel 3, regel 4 OF: regel 3, regel 4, regel 1, regel 2 – een nieuwe aanvraag kan pas worden gestart nadat de vorige is voltooid.
De belangrijkste afweging bij het kiezen tussen reentrant- en niet-rereentrantkorrels is de codecomplexiteit van het correct maken van interleaving en de moeilijkheid om erover te redeneren.
In een triviaal geval, als de korrels staatloos zijn en de logica eenvoudig, zou het in het algemeen efficiënter zijn om minder, maar voldoende herinvoerbare korrels te gebruiken zodat alle hardwarethreads worden benut.
Als de code complexer is, zou een groter aantal niet-reentrant korrels, ook al is de code over het algemeen iets minder efficiënt, u veel moeite kunnen besparen bij het opsporen van niet voor de hand liggende interleavingsproblemen.
Uiteindelijk is het antwoord afhankelijk van de specifieke kenmerken van de toepassing.
Interleaving-methoden
Korrelinterfacemethoden die zijn gemarkeerd met AlwaysInterleaveAttribute, worden altijd verweven met elke andere aanvraag en kunnen altijd worden verweven met andere aanvragen, zelfs aanvragen voor niet-[AlwaysInterleave]-methoden.
Kijk een naar het volgende voorbeeld:
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));
}
}
Houd rekening met de oproepstroom die is geïnitieerd door de volgende clientaanvraag:
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());
Aanroepen naar GoSlow
zijn niet afgewisseld, dus de totale uitvoeringstijd van de twee GoSlow
aanroepen is ongeveer 20 seconden. Aan de andere kant wordt GoFast
gemarkeerd AlwaysInterleaveAttributeen worden de drie aanroepen ervan gelijktijdig uitgevoerd, waarbij het in ongeveer 10 seconden totaal wordt voltooid in plaats van dat er ten minste 30 seconden nodig zijn om te voltooien.
Leesmethoden
Wanneer een graanmethode de korrelstatus niet wijzigt, is het veilig om gelijktijdig met andere aanvragen uit te voeren. Hiermee ReadOnlyAttribute wordt aangegeven dat een methode de status van een graan niet wijzigt. Door methoden als ReadOnly
te markeren, kan Orleans uw aanvraag gelijktijdig verwerken met andere ReadOnly
aanvragen, wat de prestaties van uw app aanzienlijk kan verbeteren. Kijk een naar het volgende voorbeeld:
public interface IMyGrain : IGrainWithIntegerKey
{
Task<int> IncrementCount(int incrementBy);
[ReadOnly]
Task<int> GetCount();
}
De GetCount
methode wijzigt de korrelstatus niet, dus wordt deze gemarkeerd met ReadOnly
. Aanroepers die wachten op deze methode-aanroep worden niet geblokkeerd door andere ReadOnly
aanvragen aan de grain, en de methode geeft onmiddellijk resultaat.
Re-entrant gedrag van gespreksketens
Als een graan een methode aanroept op een ander graan, dat vervolgens terugroept naar het oorspronkelijke graan, zal de oproep leiden tot een deadlock, tenzij de oproep reentrant is. Reentrancy kan per oproepsite worden ingeschakeld met behulp van reentrancy voor oproepketens. Als u reentrancy van oproepketens wilt inschakelen, roept u de AllowCallChainReentrancy() methode aan, die een waarde retourneert die reentrance toestaat van elke beller verderop in de oproepketen totdat deze wordt verwijderd. Dit omvat hernieuwde toegang vanuit de 'grain', die de methode zelf aanroept. Kijk een naar het volgende voorbeeld:
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>());
}
}
In het voorgaande voorbeeld roept UserGrain.JoinRoom(roomName)
aan in ChatRoomGrain.OnJoinRoom(user)
, dat probeert om terug te bellen naar UserGrain.GetDisplayName()
voor het ophalen van de weergavenaam van de gebruiker. Omdat deze oproepketen een cyclus omvat, zal dit resulteren in een deadlock als het UserGrain
niet toestaat dat hertoetreding plaatsvindt met behulp van een van de ondersteunde mechanismen die in dit artikel worden besproken. In dit geval gebruiken we AllowCallChainReentrancy(), waardoor alleen roomGrain
kan terugbellen in de UserGrain
. Hiermee krijgt u nauwkeurige controle over waar en hoe reentrancy is ingeschakeld.
Als u in plaats daarvan de impasse zou voorkomen door de GetDisplayName()
-methodedeclaratie op IUserGrain
met [AlwaysInterleave]
te annoteren, zou u processen toestaan om een GetDisplayName
-aanroep af te wisselen met een andere methode. In plaats daarvan kunt u alleenroomGrain
methoden aanroepen op ons graan en alleen totdat scope
het wordt verwijderd.
Onderdrukking van reentrant gedrag in de oproepketen
Oproepketenherentrance kan ook worden onderdrukt met behulp van de SuppressCallChainReentrancy() methode. Deze functionaliteit heeft beperkte bruikbaarheid voor eindontwikkelaars, maar is belangrijk voor intern gebruik door bibliotheken die de functionaliteit van graanobjecten uitbreiden, zoals streaming en broadcastkanalen, om te waarborgen dat ontwikkelaars volledige controle behouden over het moment waarop herinvoeren van oproepketens ingeschakeld is.
Reentrancy met een predicaat
Graanklassen kunnen een predicaat opgeven om interleaving per aanroep te bepalen door de aanvraag te inspecteren. Het [MayInterleave(string methodName)]
kenmerk biedt deze functionaliteit. Het argument voor het kenmerk is de naam van een statische methode binnen de grain-klasse die een InvokeMethodRequest object accepteert en een bool
retourneert die aangeeft of de aanvraag al dan niet moet worden geïnterleaved.
Hier volgt een voorbeeld dat interleaving toestaat als het type aanvraagargument het [Interleave]
kenmerk heeft:
[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.
}
}