Lezen in het Engels

Delen via


Aanvraagplanning

Korrelactiveringen hebben een uitvoeringsmodel met één thread en verwerken standaard elke aanvraag van begin tot voltooiing 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 interleavinggedrag 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, A en B, zijn twee soorten korrels PingGrain betrokken. Een beller roept de volgende aanroep aan:

var a = grainFactory.GetGrain("A");
var b = grainFactory.GetGrain("B");
await a.CallOther(b);

Planningsdiagram voor reentrancy.

De uitvoeringsstroom is als volgt:

  1. De oproep komt aan bij A, die een aanroep naar B registreert "1" en vervolgens een aanroep naar B uitgeeft.
  2. B keert onmiddellijk terug van Ping() terug naar A.
  3. Een logboek "2" en keert terug naar de oorspronkelijke beller.

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

Planningsdiagram voor reentrancy zonder impasse.

In dit voorbeeld:

  1. De Ping() oproep van A arriveert bij B voordat de CallOther(a) oproep bij B aankomt.
  2. Daarom verwerkt B de Ping() aanroep vóór de CallOther(a) aanroep.
  3. Omdat B de Ping() aanroep verwerkt, kan A teruggaan naar de beller.
  4. 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.
  5. 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 impasse vanwege iets andere timing.

Case 2: de oproep impasse

Planningsdiagram voor reentrancy met impasse.

In dit voorbeeld:

  1. De CallOther aanroepen komen aan bij hun respectieve korrels en worden tegelijkertijd verwerkt.
  2. Beide graanlogboeken "1" en doorgaan met await other.Ping().
  3. Omdat beide korrels nog bezig zijn (de CallOther aanvraag verwerken, wat nog niet is voltooid), wachten de Ping() aanvragen
  4. Na een tijdje bepaalt Orleans u dat er een time-out optreedt voor de oproep en dat elke Ping() oproep resulteert in een uitzondering die wordt gegenereerd.
  5. De CallOther hoofdtekst van de methode verwerkt de uitzondering niet en het belt naar 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 kostenpost, die eerder werd besproken, voornamelijk liveness: bepaalde oproeppatronen kunnen leiden tot impasses. 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 Orleans biedt ontwikkelaars opties waarmee sommige of alle aanvragen gelijktijdig kunnen worden uitgevoerd, waardoor hun uitvoering met elkaar wordt verbonden. In Orleans, dergelijke zorgen worden aangeduid als reentrancy of interleaving. Door gelijktijdig aanvragen uit te voeren, kunnen korrels die asynchrone bewerkingen uitvoeren meer aanvragen in een kortere periode verwerken.

In de volgende gevallen kunnen meerdere aanvragen worden verwerkt:

Met reentrancy wordt het volgende geval een geldige uitvoering en wordt de mogelijkheid van de bovenstaande impasse verwijderd.

Case 3: het graan of de methode is opnieuw aan het reentrant

Planningsdiagram voor re-entrancy met re-entrant grain of methode.

In dit voorbeeld kunnen korrels A en B elkaar tegelijkertijd aanroepen zonder mogelijke impasses voor het plannen van aanvragen, omdat beide korrels opnieuw worden ingeschreven. In de volgende secties vindt u meer informatie over reentrancy.

Re-entrant grains

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 grain code voert nooit meerdere stukjes graancode parallel uit (uitvoering van graancode is altijd één threaded), maar re-entrant grains kunnen de uitvoering van code voor verschillende aanvragen zien interleaving. Dat wil gezegd, de voortzetting verandert van verschillende aanvragen kan interleave.

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 Foo en Bar interleave worden uitgevoerdReentrantAttribute.

De volgende uitvoeringsvolgorde is bijvoorbeeld mogelijk:

Regel 1, regel 3, regel 2 en regel 4. Dat wil gezegd, de beurt van verschillende aanvragen interleave.

Als de korrel niet opnieuw was ingeschreven, zijn de enige mogelijke uitvoeringen: regel 1, regel 2, regel 3, regel 4 OF: regel 3, regel 4, regel 1, regel 2 (een nieuwe aanvraag kan niet worden gestart voordat 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 wanneer de korrels staatloos zijn en de logica eenvoudig is, minder (maar niet te weinig, zodat alle hardwarethreads worden gebruikt) moeten re-entrantkorrels in het algemeen iets efficiënter zijn.

Als de code complexer is, moet een groter aantal niet-reentrantkorrels, zelfs als de code iets minder efficiënt is, u veel verdriet besparen bij het opsporen van niet-onopvallende interleavingsproblemen.

Uiteindelijk is het antwoord afhankelijk van de specifieke kenmerken van de toepassing.

Interleaving-methoden

Korrelinterfacemethoden die zijn gemarkeerd met AlwaysInterleaveAttribute, interleaves elke andere aanvraag en kunnen altijd worden geinterleaed 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 om GoSlow niet te interleaved zijn, dus de totale uitvoeringstijd van de twee GoSlow aanroepen duurt 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 te markeren, zodat ReadOnly Orleans uw aanvraag gelijktijdig kan worden verwerkt met andere ReadOnly aanvragen, waardoor de prestaties van uw app aanzienlijk kunnen worden verbeterd. Kijk een naar het volgende voorbeeld:

public interface IMyGrain : IGrainWithIntegerKey
{
    Task<int> IncrementCount(int incrementBy);

    [ReadOnly]
    Task<int> GetCount();
}

Reentrancy van gespreksketen

Als een graan een methode aanroept die op een ander graan wordt aangeroepen die vervolgens weer in het oorspronkelijke graan wordt aangeroepen, resulteert de aanroep in een impasse, tenzij de aanroep opnieuw wordt aangeroepen. 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 reentrance van het graan dat 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 UserGrain.JoinRoom(roomName) wordt aangeroepen ChatRoomGrain.OnJoinRoom(user), waarin wordt geprobeerd terug UserGrain.GetDisplayName() te bellen om de weergavenaam van de gebruiker op te halen. Omdat deze oproepketen een cyclus omvat, resulteert dit in een impasse als het UserGrain niet toestaan van herverkening met behulp van een van de ondersteunde mechanismen die in dit artikel worden besproken. In dit geval gebruiken AllowCallChainReentrancy()we , waardoor we alleen roomGrain terug kunnen bellen naar de UserGrain. Hiermee krijgt u nauwkeurige controle over waar en hoe reentrancy is ingeschakeld.

Als u in plaats daarvan de impasse zou voorkomen door aantekeningen te maken bij de GetDisplayName() methodedeclaratie IUserGrain , [AlwaysInterleave]zou u korrels toestaan om een GetDisplayName aanroep te interleaveen met een andere methode. In plaats daarvan kunt u alleen roomGrain methoden aanroepen op ons graan en alleen totdat scope het wordt verwijderd.

Oproepketenherentrancy onderdrukken

Oproepketenherentrance kan ook worden onderdrukt met behulp van de SuppressCallChainReentrancy() methode. Dit heeft beperkte bruikbaarheid voor ontwikkelaars, maar het is belangrijk voor intern gebruik door bibliotheken die de korrelfunctionaliteit uitbreiden Orleans , zoals streaming - en broadcastkanalen , om ervoor te zorgen dat ontwikkelaars volledige controle behouden wanneer reentrancy voor oproepketens is ingeschakeld.

De GetCount methode wijzigt de korrelstatus niet, dus wordt deze gemarkeerd met ReadOnly. Bellers die wachten op deze methode worden niet geblokkeerd door andere ReadOnly aanvragen voor het graan en de methode wordt onmiddellijk geretourneerd.

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 die een InvokeMethodRequest object accepteert en retourneert een bool indicatie of de aanvraag al dan niet moet worden geinterleaved.

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.
    }
}