Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
Grain-activeringen hebben een uitvoeringsmodel met één thread . Standaard verwerken ze elke aanvraag van begin tot voltooiing voordat de volgende aanvraag kan worden verwerkt. In sommige gevallen kan het wenselijk zijn dat een activering andere aanvragen verwerkt terwijl één aanvraag wacht tot een asynchrone bewerking is voltooid. Orleans geeft u, om deze en andere redenen, enige controle over het gedrag van het door elkaar verwerken 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 ongelukkige reeks gebeurtenissen waarbij dezelfde code resulteert in een deadlock door een iets andere timing.
Case 2: De oproepen lopen vast
In dit voorbeeld:
- De
CallOtheraanroepen komen aan bij hun respectieve korrels en worden tegelijkertijd verwerkt. - Beide graanlogboeken in
"1"en ga verder naarawait other.Ping(). - Omdat beide processen nog bezig zijn (met het
CallOtherverzoek verwerken, wat nog niet is voltooid), wachten dePing()verzoeken. - Na een tijdje bepaalt Orleans dat er een time-out is voor de oproep, en dat elke
Ping()oproep resulteert in het genereren van een uitzondering. - De methodebody
CallOtherverwerkt de uitzondering niet en deze borrelt omhoog naar de oorspronkelijke aanroeper.
In de volgende sectie wordt beschreven hoe u impasses kunt voorkomen door meerdere verzoeken toe te staan hun uitvoering te verweven.
Herintreding
Orleans is standaard ingesteld op een veilige uitvoeringsstroom waarbij de interne status van een graan niet gelijktijdig wordt gewijzigd door meerdere aanvragen. Gelijktijdige wijziging bemoeilijkt logica en zorgt voor een grotere last voor u, de ontwikkelaar. Deze bescherming tegen gelijktijdigheidsfouten brengt kosten met zich mee, met name liveness: bepaalde oproeppatronen kunnen leiden tot deadlocks, zoals eerder besproken. Een manier om impasses te voorkomen, is ervoor te zorgen dat graanoproepen nooit een cyclus vormen. Vaak is het moeilijk om code te schrijven die cyclusvrij is en gegarandeerd geen impasse heeft. Als u wacht totdat elke aanvraag van begin tot voltooiing wordt uitgevoerd voordat de volgende bewerking 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 databaseantwoord binnenkomt.
Elk van deze gevallen wordt in de volgende secties besproken. Om deze redenen, Orleans biedt opties om sommige of alle aanvragen gelijktijdig uit te voeren, waardoor hun uitvoering elkaar doorkruist. In Orleans, verwijzen we naar dergelijke zorgen als reentrancy of interleaving. Door gelijktijdig aanvragen uit te voeren, kunnen entiteiten die asynchrone bewerkingen uitvoeren, in staat zijn meer aanvragen te verwerken in een kortere periode.
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, waarbij de mogelijkheid van de hierboven beschreven impasse wordt verwijderd.
Case 3: Het graan of de methode is opnieuw aan het reentrant
In dit voorbeeld kunnen grains A en B elkaar tegelijkertijd aanroepen zonder mogelijke deadlocks bij het plannen van aanvragen, omdat beide grains reentrant zijn. In de volgende secties vindt u meer informatie over reentrancy.
Inschapende korrels
U kunt Grain-implementatieklassen markeren met de ReentrantAttribute om aan te geven dat verschillende aanvragen vrijelijk kunnen worden verweven.
Met andere woorden, een nieuwe activering kan beginnen met het verwerken van een andere aanvraag terwijl een vorige aanvraag niet is voltooid. De uitvoering is nog steeds beperkt tot één thread, dus de activering voert één beurt tegelijk uit en elke beurt wordt uitgevoerd namens slechts één van de verzoeken van de activering.
Reentrant graancode voert nooit meerdere stukjes graancode parallel uit (uitvoering is altijd single-threaded), maar reentrant grains kunnen zien dat de uitvoering van code voor verschillende aanvragen verweven raakt. Dat wil zeggen dat de voortzetting van verschillende aanvragen in elkaar kan grijpen.
Denk bijvoorbeeld aan twee methoden van dezelfde graanklasse, zoals wordt weergegeven in de volgende pseudocode FooBar :
Task Foo()
{
await task1; // line 1
return Do2(); // line 2
}
Task Bar()
{
await task2; // line 3
return Do2(); // line 4
}
Als dit graan ReentrantAttribute is gemarkeerd, kan de uitvoering van Foo en Bar worden afgewisseld.
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-reentrantkorrels is de codecomplexiteit die nodig is om interleaving correct te laten werken en de moeilijkheid om erover te redeneren.
In een triviaal geval waarin korrels staatloos zijn en de logica eenvoudig is, moet het gebruik van minder reentrantkorrels (maar niet te weinig, zodat alle hardwarethreads worden gebruikt) over het algemeen iets efficiënter zijn.
Als de code complexer is, kan het gebruik van een groter aantal niet-reentrantkorrels, zelfs als de code iets minder efficiënt is, u mogelijk aanzienlijke problemen 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
nl-NL: Grain interface-methoden die zijn gemarkeerd met AlwaysInterleaveAttribute kunnen altijd interleaved worden met elke andere aanvraag en kunnen altijd worden geïnterleaveerd door elke andere aanvraag, zelfs aanvragen voor methoden zonder markering AlwaysInterleaveAttribute.
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());
Oproepen naar GoSlow zijn niet overlappend, dus de totale uitvoeringstijd van de twee aanroepen GoSlow is ongeveer 20 seconden. Aan de andere kant is GoFast als AlwaysInterleaveAttribute gemarkeerd. De drie oproepen worden gelijktijdig uitgevoerd, waardoor het geheel ongeveer 10 seconden duurt in plaats van ten minste 30 seconden.
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 het graan niet wijzigt. Door methoden als ReadOnlyAttribute te markeren, kan Orleans uw request gelijktijdig met andere ReadOnlyAttribute requests verwerken, 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 ReadOnlyAttribute. Aanroepers die wachten op deze methode-aanroep worden niet geblokkeerd door andere ReadOnlyAttribute 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 weer in het oorspronkelijke graan aanroept, resulteert de aanroep in een impasse, tenzij de aanroep opnieuw wordt aangeroepen. U kunt reentrancy per oproepsite inschakelen met behulp van reentrancy voor oproepketens. Als u reentrancy voor oproepketens wilt inschakelen, roept u de AllowCallChainReentrancy() methode aan. Deze methode retourneert een waarde die reentrance toestaat van elke aanroeper verderop in de oproepketen totdat de waarde 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)ChatRoomGrain.OnJoinRoom(user) aan, dat probeert UserGrain.GetDisplayName() terug te bellen om de weergavenaam van de gebruiker op te halen. Omdat deze oproepketen een cyclus omvat, resulteert dit in een impasse als UserGrain het niet toestaat dat er een van de ondersteunde mechanismen wordt gebruikt die in dit artikel worden besproken. In dit geval gebruiken we AllowCallChainReentrancy(), waardoor alleen roomGrain kan terugroepen naar UserGrain. Dit geeft u gedetailleerde controle over waar en hoe reentrancy wordt ingeschakeld.
Als u de impasse zou voorkomen door in plaats daarvan de GetDisplayName() methodedeclaratie op IUserGrain van een AlwaysInterleaveAttribute-annotatie te voorzien, zou u elke graan toestaan om een aanroep met een andere methode af te wisselen. Door te gebruiken AllowCallChainReentrancy, staat u alleenroomGrain toe om methoden aan te roepen op de UserGrain, en alleen totdat scope wordt verwijderd.
Onderdrukking van reentrant gedrag in de oproepketen
U kunt oproepketenherentrance ook onderdrukken met behulp van de SuppressCallChainReentrancy() methode. Dit heeft beperkte bruikbaarheid voor eindontwikkelaars, maar is belangrijk voor intern gebruik door bibliotheken die de Orleans-korrelfunctionaliteit uitbreiden, zoals streaming en broadcastkanalen, om ervoor te zorgen dat ontwikkelaars volledige controle behouden over wanneer re-entrantie voor oproepketens is ingeschakeld.
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 graanklasse. Deze methode accepteert een InvokeMethodRequest object en retourneert een bool waarde die aangeeft of de aanvraag moet worden afgewisseld.
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.
}
}