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
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.
Både Task.Run och ombudet
endMethod
för TaskFactory.FromAsync respekterar inte den aktuella schemaläggaren. Båda använderTaskScheduler.Default
schemaläggaren, som är schemaläggaren för .NET-trådpoolen. Därför körs koden inutiTask.Run
ochendMethod
inTask.Factory.FromAsync
alltid på .NET-trådpoolen utanför den enkeltrådade körningsmodellen för Orleans korn. Men all kod efterawait Task.Run
ellerawait Task.Factory.FromAsync
kommer att köras tillbaka under schemaläggaren vid den tidpunkt då aktiviteten skapades, vilket är kornets schemaläggare.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.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örList<T>.ForEach(async element => ...)
och alla andra metoder som accepterar en Action<T>, eftersom den asynkrona delegaten kommer att tvingas till ettasync 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.StartNew
fö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.Run
och 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. |