Dela via


Schemaläggning av begäranden

Kornaktiveringar har en entrådad körningsmodell och bearbetar som standard varje begäran från början till slut innan nästa begäran kan börja bearbetas. I vissa fall kan det vara önskvärt att aktiveringen bearbetar andra begäranden medan en begäran väntar på att en asynkron åtgärd ska slutföras. Av detta och andra skäl Orleans ger utvecklaren viss kontroll över beteendet för interfoliering av begäranden, enligt beskrivningen i avsnittet Reentrancy . Följande är ett exempel på schemaläggning av begäranden som inte skickas på nytt, vilket är standardbeteendet i Orleans.

Överväg följande PingGrain definition:

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");
    }
}

Två typer av korn PingGrain ingår i vårt exempel, A och B. En anropare anropar följande anrop:

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

Schemaläggningsdiagram för återaktivering.

Körningsflödet är följande:

  1. Samtalet kommer till A, som loggar "1" och sedan utfärdar ett anrop till B.
  2. B returnerar omedelbart från Ping() tillbaka till A.
  3. En loggar "2" och återgår till den ursprungliga anroparen.

Medan A väntar på anropet till B kan det inte bearbeta några inkommande begäranden. Om A och B anropar varandra samtidigt kan de därför vara låsta i väntan på att dessa anrop ska slutföras. Här är ett exempel, baserat på klienten som utfärdar följande anrop:

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));

Fall 1: anropen inte dödläge

Schemaläggningsdiagram för återaktivering utan dödläge.

I det här exemplet:

  1. Samtalet Ping() från A kommer till B innan samtalet CallOther(a) kommer till B.
  2. Därför bearbetar B anropet Ping() före anropetCallOther(a).
  3. Eftersom B bearbetar anropet Ping() kan A återgå till anroparen.
  4. När B utfärdar sitt Ping() anrop till A är A fortfarande upptagen med att logga sitt meddelande ("2"), så anropet måste vänta en kort tid, men det kan snart bearbetas.
  5. A bearbetar anropet Ping() och återgår till B, som återgår till den ursprungliga anroparen.

Överväg en mindre lyckligt lottad serie händelser: en där samma kod resulterar i ett dödläge på grund av något annorlunda tidpunkt.

Fall 2: anropens dödläge

Schemaläggningsdiagram för återaktivering med dödläge.

I det här exemplet:

  1. Anropen CallOther anländer till respektive korn och bearbetas samtidigt.
  2. Båda kornloggarna loggar "1" och fortsätter till await other.Ping().
  3. Eftersom båda kornen fortfarande är upptagna (bearbetning av CallOther begäran, som inte har slutförts ännu), väntar begärandena Ping()
  4. Efter ett tag Orleans avgörs att anropet har överskridits och att varje Ping() anrop resulterar i att ett undantag utlöses.
  5. Metodtexten CallOther hanterar inte undantaget och bubblar upp till den ursprungliga anroparen.

I följande avsnitt beskrivs hur du förhindrar dödlägen genom att tillåta flera begäranden att interleave sin körning med varandra.

Återinträde

Orleans standardvärdet för att välja ett säkert körningsflöde: ett där det interna tillståndet för ett korn inte ändras samtidigt under flera begäranden. Samtidig ändring av det interna tillståndet komplicerar logiken och lägger större börda på utvecklaren. Det här skyddet mot dessa typer av samtidighetsbuggar har en kostnad som tidigare diskuterats, främst liveness: vissa anropsmönster kan leda till dödlägen. Ett sätt att undvika dödlägen är att se till att kornanrop aldrig resulterar i en cykel. Ofta är det svårt att skriva kod som är cykelfri och inte kan blockeras. Att vänta på att varje begäran ska köras från början till slut innan nästa begäran bearbetas kan också skada prestandan. Om en kornmetod till exempel utför en asynkron begäran till en databastjänst pausar kornigheten körningen av begäran tills svaret från databasen kommer till kornigheten.

Vart och ett av dessa fall beskrivs i de avsnitt som följer. Av dessa skäl Orleans ger utvecklare alternativ för att tillåta att vissa eller alla begäranden körs samtidigt, vilket mellanlagrar deras körning med varandra. I Orleanskallas sådana problem för återaktivering eller interleaving. Genom att köra begäranden samtidigt kan korn som utför asynkrona åtgärder bearbeta fler begäranden under en kortare period.

Flera begäranden kan interfolieras i följande fall:

Med reentrancy blir följande fall en giltig körning och risken för ovanstående dödläge tas bort.

Fall 3: kornet eller metoden är reentrant

Schemaläggningsdiagram för återaktivering med nytt deltagarintervall eller -metod.

I det här exemplet kan korn A och B anropa varandra samtidigt utan någon potential för att schemalägga dödlägen för begäranden eftersom båda kornen är nya deltagare. Följande avsnitt innehåller mer information om återaktivering.

Nytt deltagarintervall

Implementeringsklasserna Grain kan markeras med ReentrantAttribute för att indikera att olika begäranden kan vara fritt mellanlagrade.

Med andra ord kan en aktivering av ny deltagare börja köra en annan begäran medan en tidigare begäran inte har slutfört bearbetningen. Körningen är fortfarande begränsad till en enda tråd, så aktiveringen körs fortfarande en tur i taget och varje tur körs för endast en av aktiveringens begäranden.

Kornkoden för ny deltagare kör aldrig flera delar av kornkoden parallellt (körningen av kornkod är alltid enkeltrådad), men omtilldelande korn kan se körningen av kod för olika begäranden som interfolierar. Det vill sa att fortsättningen ändras från olika begäranden kan interleave.

Som du ser i följande pseudokod bör du till exempel tänka på att Foo och Bar är två metoder för samma kornklass:

Task Foo()
{
    await task1;    // line 1
    return Do2();   // line 2
}

Task Bar()
{
    await task2;   // line 3
    return Do2();  // line 4
}

Om det här kornet är markerat ReentrantAttributekan körningen av Foo och Bar mellanlagras.

Följande körningsordning är till exempel möjlig:

Rad 1, rad 3, linje 2 och rad 4. Det vill: svängarna från olika begäranden interleave.

Om kornet inte var en ny deltagare skulle de enda möjliga körningarna vara: rad 1, rad 2, rad 3, rad 4 ELLER: rad 3, rad 4, rad 1, rad 2 (en ny begäran kan inte starta innan den föregående har slutförts).

Den största kompromissen med att välja mellan reentrant- och icke-eftersläpande korn är kodkomplexiteten att få interfoliering att fungera korrekt och svårigheten att resonera om det.

I ett trivialt fall när kornen är tillståndslösa och logiken är enkel, färre (men inte för få, så att alla maskinvarutrådar används) bör ny deltagare i allmänhet vara något effektivare.

Om koden är mer komplex bör ett större antal icke-reentrantkorn, även om de är något mindre effektiva totalt sett, spara mycket sorg när du tar reda på icke-lydiga interleaving-problem.

I slutändan beror svaret på programmets specifika egenskaper.

Interleaving-metoder

Grain-gränssnittsmetoder som är markerade med AlwaysInterleaveAttribute, mellanläser alltid alla andra begäranden och kan alltid interfolieras med andra begäranden, även begäranden för icke-[AlwaysInterleave]-metoder.

Ta följande som exempel:

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));
    }
}

Överväg det anropsflöde som initieras av följande klientbegäran:

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());

Anrop till GoSlow interfolieras inte, så den totala körningstiden för de två GoSlow anropen tar cirka 20 sekunder. Å andra sidan GoFast är markerat AlwaysInterleaveAttribute, och de tre anropen till den körs samtidigt, vilket slutförs i cirka 10 sekunder totalt i stället för att kräva minst 30 sekunder för att slutföra.

Skrivskyddade metoder

När en kornmetod inte ändrar korntillståndet är det säkert att köra samtidigt med andra begäranden. ReadOnlyAttribute Anger att en metod inte ändrar tillståndet för ett korn. Märkningsmetoder som ReadOnly gör det möjligt Orleans att bearbeta din begäran samtidigt med andra ReadOnly begäranden, vilket avsevärt kan förbättra appens prestanda. Ta följande som exempel:

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

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

Återaktivering av samtalskedja

Om ett korn anropar en metod som på ett annat korn som sedan anropar tillbaka till det ursprungliga kornet resulterar anropet i ett dödläge om inte anropet är reentrant. Återaktivering kan aktiveras per anropswebbplats med hjälp av återaktivering av samtalskedja. Om du vill aktivera återaktivering av anropskedjan anropar AllowCallChainReentrancy() du metoden, som returnerar ett värde som tillåter återaktivering från alla anropare längre ned i anropskedjan tills den tas bort. Detta inkluderar reentrance från kornet som anropar själva metoden. Ta följande som exempel:

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>());
    }
}

I föregående exempel UserGrain.JoinRoom(roomName) anropar till ChatRoomGrain.OnJoinRoom(user), som försöker anropa tillbaka UserGrain.GetDisplayName() till för att hämta användarens visningsnamn. Eftersom den här anropskedjan omfattar en cykel resulterar detta i ett dödläge om UserGrain inte tillåter reentrance med någon av de mekanismer som stöds som beskrivs i den här artikeln. I det här fallet använder AllowCallChainReentrancy()vi , som endast roomGrain tillåter att anropa tillbaka till UserGrain. Detta ger dig detaljerad kontroll över var och hur återaktivering är aktiverat.

Om du i stället skulle förhindra dödläget genom att kommentera metoddeklarationen GetDisplayName()IUserGrain med [AlwaysInterleave], skulle du tillåta alla korn att interleave ett GetDisplayName anrop med någon annan metod. I stället tillåter du bara roomGrain att anropa metoder på vårt korn och endast tills scope de tas bort.

Ignorera återaktivering av samtalskedja

Återaktivering av samtalskedja kan också ignoreras med hjälp av SuppressCallChainReentrancy() metoden . Detta har begränsad användbarhet för slututvecklare, men det är viktigt för internt bruk av bibliotek som utökar Orleans kornfunktioner, till exempel strömnings - och sändningskanaler för att säkerställa att utvecklare behåller fullständig kontroll över när återaktivering av samtalskedja är aktiverat.

Metoden GetCount ändrar inte korntillståndet, så den är markerad med ReadOnly. Anropare som väntar på den här metodens anrop blockeras inte av andra ReadOnly begäranden till kornet, och metoden returnerar omedelbart.

Reentrancy med hjälp av ett predikat

Kornklasser kan ange ett predikat för att fastställa interleaving på anropsbasis genom att granska begäran. Attributet [MayInterleave(string methodName)] tillhandahåller den här funktionen. Argumentet till attributet är namnet på en statisk metod inom kornklassen som accepterar ett InvokeMethodRequest objekt och returnerar ett bool som anger om begäran ska interfolieras eller inte.

Här är ett exempel som tillåter interfoliering om argumenttypen för begäran har [Interleave] attributet:

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