Dela via


Externa uppgifter och korn

Designvis skickas alla underaktiviteter som skapas från kornkod (till exempel med hjälp await av eller ContinueWith Task.Factory.StartNew) på samma per aktivering TaskScheduler som den överordnade aktiviteten och ärver därför samma körningsmodell med en tråd som resten av kornkoden. Det här är huvudpunkten bakom den enkeltrådade körningen av kornig turbaserad samtidighet.

I vissa fall kan kornkod behöva "bryta ut" av schemaläggningsmodellen Orleans och "göra något speciellt", till exempel att uttryckligen peka en Task till en annan schemaläggare eller .NET ThreadPool. Ett exempel på ett sådant fall är när kornkod måste köra ett synkront fjärrblockeringsanrop (till exempel fjärr-I/O). Om du kör det blockeringsanropet i kornkontexten blockeras kornigheten och bör därför aldrig göras. I stället kan kornkoden köra den här blockeringskoden i trådpoolstråden och koppla (await) slutföra körningen och fortsätta i kornkontexten. Vi förväntar oss att det är ett mycket avancerat och sällan obligatoriskt användningsscenario att fly från Orleans schemaläggaren utöver de "normala" användningsmönstren.

Aktivitetsbaserade API:er

  1. await, TaskFactory.StartNew (se nedan), Task.ContinueWith, Task.WhenAny, Task.WhenAll, Task.Delay alla respekterar den aktuella schemaläggaren. Det innebär att användning av dem på standardsättet, utan att skicka en annan TaskScheduler, gör att de körs i kornkontexten.

  2. Både Task.Run och ombudet endMethod för TaskFactory.FromAsync respekterar inte den aktuella schemaläggaren. Båda använder TaskScheduler.Default schemaläggaren, som är schemaläggaren för .NET-trådpoolen. Därför körs koden inuti Task.Run och endMethod in Task.Factory.FromAsync alltid på .NET-trådpoolen utanför den enkeltrådade körningsmodellen för Orleans korn. Men all kod efter await Task.Run eller await Task.Factory.FromAsync kommer att köras tillbaka under schemaläggaren vid den tidpunkt då aktiviteten skapades, vilket är kornets schemaläggare.

  3. Task.ConfigureAwait med false är ett explicit API för att undkomma den aktuella schemaläggaren. Det gör att koden efter en väntande uppgift körs på TaskScheduler.Default schemaläggaren, som är .NET-trådpoolen, och därmed bryter den enkeltrådade körningen av kornet.

    Varning

    Du bör i allmänhet aldrig använda ConfigureAwait(false) direkt i kornkod.

  4. Metoder med signaturen async void bör inte användas med korn. De är avsedda för grafiska händelsehanterare för användargränssnittet. async void -metoden kan omedelbart krascha den aktuella processen om de tillåter att ett undantag kommer undan, utan något sätt att hantera undantaget. Detta gäller även för List<T>.ForEach(async element => ...) och alla andra metoder som accepterar en Action<T>, eftersom den asynkrona delegaten kommer att tvingas till ett async void ombud.

Task.Factory.StartNew och async ombud

Den vanliga rekommendationen för schemaläggning av uppgifter i alla C#-program är att använda Task.Run till förmån Task.Factory.StartNewför . En snabb Google-sökning om användningen av Task.Factory.StartNew kommer att föreslå att det är farligt och att man alltid bör gynna Task.Run. Men om vi vill stanna kvar i kornets entrådade körningsmodell för vårt korn måste vi använda den, så hur gör vi det på rätt sätt då? Risken när du använder Task.Factory.StartNew() är att den inte har inbyggt stöd för asynkrona ombud. Det innebär att detta sannolikt är en bugg: var notIntendedTask = Task.Factory.StartNew(SomeDelegateAsync). notIntendedTask är inte en uppgift som slutförs när SomeDelegateAsync den gör det. I stället bör man alltid packa upp den returnerade uppgiften: var task = Task.Factory.StartNew(SomeDelegateAsync).Unwrap().

Exempel på flera aktiviteter och schemaläggaren

Nedan visas exempelkod som visar användningen av TaskScheduler.Current, Task.Runoch en särskild anpassad schemaläggare för att fly från Orleans kornkontexten och hur du kommer tillbaka till den.

public async Task MyGrainMethod()
{
    // Grab the grain's task scheduler
    var orleansTS = TaskScheduler.Current;
    await Task.Delay(10_000);

    // Current task scheduler did not change, the code after await is still running
    // in the same task scheduler.
    Assert.AreEqual(orleansTS, TaskScheduler.Current);

    Task t1 = Task.Run(() =>
    {
        // This code runs on the thread pool scheduler, not on Orleans task scheduler
        Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
        Assert.AreEqual(TaskScheduler.Default, TaskScheduler.Current);
    });

    await t1;

    // We are back to the Orleans task scheduler.
    // Since await was executed in Orleans task scheduler context, we are now back
    // to that context.
    Assert.AreEqual(orleansTS, TaskScheduler.Current);

    // Example of using Task.Factory.StartNew with a custom scheduler to escape from
    // the Orleans scheduler
    Task t2 = Task.Factory.StartNew(() =>
    {
        // This code runs on the MyCustomSchedulerThatIWroteMyself scheduler, not on
        // the Orleans task scheduler
        Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
        Assert.AreEqual(MyCustomSchedulerThatIWroteMyself, TaskScheduler.Current);
    },
    CancellationToken.None,
    TaskCreationOptions.None,
    scheduler: MyCustomSchedulerThatIWroteMyself);

    await t2;

    // We are back to Orleans task scheduler.
    Assert.AreEqual(orleansTS, TaskScheduler.Current);
}

Exempel gör ett kornigt anrop från kod som körs på en trådpool

Ett annat scenario är en del av kornkoden som måste "bryta ut" av kornets uppgiftsschemaläggningsmodell och köras på en trådpool (eller någon annan kontext som inte är kornig), men som fortfarande behöver anropa ett annat korn. Kornanrop kan göras från icke-korniga kontexter utan extra ceremoni.

Följande är kod som visar hur ett kornigt anrop kan göras från en kod som körs i ett korn men inte i kornkontexten.

public async Task MyGrainMethod()
{
    // Grab the Orleans task scheduler
    var orleansTS = TaskScheduler.Current;
    var fooGrain = this.GrainFactory.GetGrain<IFooGrain>(0);
    Task<int> t1 = Task.Run(async () =>
    {
        // This code runs on the thread pool scheduler,
        // not on Orleans task scheduler
        Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
        int res = await fooGrain.MakeGrainCall();

        // This code continues on the thread pool scheduler,
        // not on the Orleans task scheduler
        Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
        return res;
    });

    int result = await t1;

    // We are back to the Orleans task scheduler.
    // Since await was executed in the Orleans task scheduler context,
    // we are now back to that context.
    Assert.AreEqual(orleansTS, TaskScheduler.Current);
}

Arbeta med bibliotek

Vissa externa bibliotek som koden använder kan användas ConfigureAwait(false) internt. Det är en bra och korrekt metod i .NET att använda ConfigureAwait(false) när du implementerar bibliotek för generell användning. Det här är inte ett problem i Orleans. Så länge koden i kornet som anropar biblioteksmetoden väntar på biblioteksanropet med en vanlig await, är kornkoden korrekt. Resultatet blir exakt som önskat – bibliotekskoden kör fortsättningar på standardschemaläggaren (värdet som returneras av TaskScheduler.Default, vilket inte garanterar att fortsättningarna körs på en ThreadPool tråd eftersom fortsättningar ofta infogas i föregående tråd), medan kornkoden körs på kornets schemaläggare.

En annan fråga som ofta ställs är om det finns ett behov av att köra biblioteksanrop med Task.Run, dvs. om det finns ett behov av att uttryckligen avlasta bibliotekskoden till ThreadPool (för kornkod att göra Task.Run(() => myLibrary.FooAsync())). Svaret är nej. Det finns inget behov av att avlasta någon kod till ThreadPool förutom när det gäller bibliotekskod som gör ett blockerande synkront anrop. Vanligtvis gör inte alla välskrivna och korrekta .NET-asynkrona bibliotek (metoder som returnerar Task och namnges med ett Async suffix) blockeringsanrop. Därför behöver du inte avlasta något till om du inte misstänker att ThreadPool async-biblioteket är buggigt eller om du avsiktligt använder ett synkront blockeringsbibliotek.

Dödlägen

Eftersom korn körs på ett enkeltrådat sätt är det möjligt att blockera ett korn genom att synkront blockera på ett sätt som skulle kräva flera trådar för att avblockera. Det innebär att kod som anropar någon av följande metoder och egenskaper kan blockera ett korn om de angivna uppgifterna ännu inte har slutförts när metoden eller egenskapen anropas:

  • Task.Wait()
  • Task.Result
  • Task.WaitAny(...)
  • Task.WaitAll(...)
  • task.GetAwaiter().GetResult()

Dessa metoder bör undvikas i alla tjänster med hög samtidighet eftersom de kan leda till dåliga prestanda och instabilitet genom att svälta .NET ThreadPool genom att blockera trådar som kan utföra användbart arbete och kräva att .NET ThreadPool matar in ytterligare trådar så att de kan slutföras. När du kör kornkod kan dessa metoder, som nämnts ovan, leda till att kornet hamnar i ett dödläge, och därför bör de också undvikas i kornkod.

Om det finns vissa sync-over-async-arbeten som inte kan undvikas är det bäst att flytta det arbetet till en separat schemaläggare. Det enklaste sättet att göra detta är att till exempel använda await Task.Run(() => task.Wait()) . Observera att vi rekommenderar att du undviker synkronisering över asynkront arbete eftersom det, som nämnts ovan, gör att programmets skalbarhet och prestanda blir lidande.

Sammanfattning av arbete med uppgifter i Orleans

Vad försöker du göra? Hur du gör det.
Kör bakgrundsarbete på .NET-trådpoolstrådar. Ingen kornkod eller korniga anrop tillåts. Task.Run
Kör asynkron arbetsuppgift från kornkod med Orleans turbaserade samtidighetsgarantier (se ovan). Task.Factory.StartNew(WorkerAsync).Unwrap() (Unwrap)
Kör synkron arbetsuppgift från kornkod med Orleans turbaserade samtidighetsgarantier. Task.Factory.StartNew(WorkerSync)
Tidsgränser för körning av arbetsobjekt Task.Delay + Task.WhenAny
Anropa en asynkron biblioteksmetod await biblioteksanropet
Använda async/await Den normala programmeringsmodellen .NET Task-Async. Stöds och rekommenderas
ConfigureAwait(false) Använd inte invändig kornkod. Tillåts endast i bibliotek.