Panoramica della pianificazione
In Orleans esistono due forme di pianificazione rilevanti per i grani:
- Pianificazione delle richieste, ovvero la pianificazione delle chiamate ai grani in ingresso per l'esecuzione in base alle regole di pianificazione illustrate in Pianificazione delle richieste.
- Pianificazione delle attività, ovvero la pianificazione dei blocchi di codice sincroni da eseguire in modalità a thread singolo.
Tutto il codice di un grano viene eseguito nell'utilità di pianificazione delle attività del grano, il che significa che anche le richieste vengono eseguite in tale utilità. Anche se le regole di pianificazione delle richieste consentono l'esecuzione simultanea di più richieste, queste ultime non vengono eseguite in parallelo perché l'utilità di pianificazione delle attività del grano esegue sempre le attività una alla volta e mai in parallelo.
Pianificazione delle attività
Per comprendere meglio la pianificazione, prendere in considerazione il grano seguente, MyGrain
, con un metodo denominato DelayExecution()
che registra un messaggio, attende un certo tempo e quindi registra un altro messaggio prima di restituire un valore.
public interface IMyGrain : IGrain
{
Task DelayExecution();
}
public class MyGrain : Grain, IMyGrain
{
private readonly ILogger<MyGrain> _logger;
public MyGrain(ILogger<MyGrain> logger) => _logger = logger;
public async Task DelayExecution()
{
_logger.LogInformation("Executing first task");
await Task.Delay(1_000);
_logger.LogInformation("Executing second task");
}
}
Il corpo di questo metodo viene eseguito in due parti:
- La prima chiamata a
_logger.LogInformation(...)
e la chiamata aTask.Delay(1_000)
. - La seconda chiamata a
_logger.LogInformation(...)
.
La seconda attività non viene pianificata nell'utilità di pianificazione delle attività del grano finché non è stata completata la chiamata a Task.Delay(1_000)
. A quel punto, viene pianificata la continuazione del metodo del grano.
Ecco una rappresentazione grafica del modo in cui una richiesta viene pianificata ed eseguita come due attività:
La descrizione precedente non è specifica di Orleans e illustra il funzionamento della pianificazione delle attività in .NET: i metodi asincroni in C# vengono convertiti in una macchina a stati asincrona dal compilatore e l'esecuzione procede attraverso la macchina a stati asincrona eseguendo passaggi discreti. Ogni passaggio viene pianificato nel TaskScheduler corrente (accessibile tramite TaskScheduler.Current e definito come TaskScheduler.Default per impostazione predefinita) o nel SynchronizationContext corrente. Se viene usato un TaskScheduler
, ogni passaggio nel metodo è rappresentato da un'istanza di Task
passata a tale TaskScheduler
. Pertanto, un oggetto Task
in .NET può rappresentare due cose:
- Un'operazione asincrona per cui è possibile rimanere in attesa del completamento. L'esecuzione del metodo
DelayExecution()
precedente è rappresentata da un oggettoTask
per cui è possibile rimanere in attesa del completamento. - In un blocco di lavoro sincrono, ogni fase all'interno del metodo
DelayExecution()
precedente è rappresentata da un oggettoTask
.
Quando TaskScheduler.Default
è in uso, le continuazioni vengono pianificate direttamente nel ThreadPool .NET e non vengono incluse in un oggetto Task
. Il wrapping delle continuazioni nelle istanze di Task
avviene in modo trasparente e pertanto gli sviluppatori raramente devono essere a conoscenza di questi dettagli di implementazione.
Pianificazione delle attività in Orleans
Ogni attivazione di grano ha una propria istanza di TaskScheduler
che è responsabile dell'applicazione del modello di esecuzione dei grani a thread singolo. Internamente, questo TaskScheduler
viene implementato tramite ActivationTaskScheduler
e WorkItemGroup
. WorkItemGroup
mantiene le attività accodate in Queue<T>, dove T
è un oggetto Task
interno e implementa IThreadPoolWorkItem. Per eseguire ogni oggetto Task
accodato, WorkItemGroup
pianifica se stesso nel ThreadPool
.NET. Quando il ThreadPool
.NET richiama il metodo IThreadPoolWorkItem.Execute()
di WorkItemGroup
, WorkItemGroup
esegue una alla volta le istanze di Task
accodate.
Ogni grano ha un'utilità di pianificazione che viene eseguita autonomamente nel ThreadPool
.NET:
Ogni utilità di pianificazione contiene una coda di attività:
Il ThreadPool
.NET esegue ogni elemento di lavoro presente in tale coda. Sono inclusi utilità di pianificazione di grani e altri elementi di lavoro, ad esempio quelli pianificati tramite Task.Run(...)
:
Nota
L'utilità di pianificazione di un grano può essere eseguita su un solo thread alla volta, ma non sempre nello stesso thread. Il ThreadPool
.NET è libero di usare un thread diverso ogni volta che viene eseguita l'utilità di pianificazione del grano. L'utilità di pianificazione del grano ha il compito di assicurarsi di essere eseguita su un solo thread alla volta e questo è il modo in cui viene implementato il modello di esecuzione a thread singolo dei grani.