Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Por diseño, las subtareas que se generan a partir del código de grano (por ejemplo, mediante await
, ContinueWith
, o Task.Factory.StartNew
) se envían en la misma activación TaskScheduler que la tarea primaria. Por lo tanto, heredan el mismo modelo de ejecución de un solo subproceso que el resto del código de grano. Este es el aspecto principal que marca la ejecución uniproceso de la simultaneidad basada en turnos de los granos.
En algunos casos, es posible que el código granular tenga que "interrumpir" el modelo de programación de tareas del Orleans y "hacer algo especial", como apuntar explícitamente un Task
a un programador de tareas diferente o al ThreadPool de .NET. Un ejemplo es cuando el código de grano necesita ejecutar una llamada de bloqueo remoto sincrónica (como la E/S remota). La ejecución de esa llamada de bloqueo en el contexto de grano bloquea el grano y, por tanto, nunca debe realizarse. En su lugar, el código de grano puede ejecutar este fragmento de código de bloqueo en un subproceso de grupo de subprocesos, unir (await
) la finalización de esa ejecución y, a continuación, continuar en el contexto de grano. Se espera que escapar del Orleans planificador sea un escenario de uso muy avanzado y rara vez necesario fuera de los patrones de uso típicos.
API basadas en tareas
await
, TaskFactory.StartNew (consulte a continuación), Task.ContinueWith, Task.WhenAny, Task.WhenAlly Task.Delay todos respetan el programador de tareas actual. Esto significa que al usarlos de forma predeterminada, sin pasar un TaskScheduler diferente, se ejecutan en un contexto específico.Tanto Task.Run como el delegado
endMethod
de TaskFactory.FromAsyncno respetan el programador de tareas actual. Ambos usan elTaskScheduler.Default
planificador, el planificador de tareas del grupo de subprocesos de .NET. Por lo tanto, el código dentro deTask.Run
y enendMethod
Task.Factory.FromAsync
siempre corre en el grupo de subprocesos de .NET, fuera del modelo de ejecución de un solo subproceso para granos de .NET. Sin embargo, cualquier código después deawait Task.Run
oawait Task.Factory.FromAsync
se ejecuta nuevamente bajo el planificador activo en el momento de creación de la tarea, que es el planificador del grano.Task.ConfigureAwait con
false
es una API explícita para obviar al programador de tareas actual. Hace que el código después de que se espereTask
se ejecute en el planificador TaskScheduler.Default (el grupo de subprocesos de .NET), lo que interrumpe la ejecución de un solo subproceso de la unidad.Precaución
Por lo general, nunca use
ConfigureAwait(false)
directamente en el código de grano.Los métodos con la firma
async void
no deben utilizarse con granos. Están diseñados para controladores de eventos de interfaces gráficas de usuario. Unasync void
método puede bloquear inmediatamente el proceso actual si permite que se escape una excepción, sin forma de controlar la excepción. Esto también se aplica aList<T>.ForEach(async element => ...)
y a cualquier otro método que acepte Action<T>, ya que el delegado asincrónico se adapta a un delegadoasync void
.
Delegados Task.Factory.StartNew
y async
La recomendación habitual para programar tareas en C# es usar Task.Run
en lugar de Task.Factory.StartNew
. Una búsqueda web rápida para Task.Factory.StartNew
sugiere que es peligroso y siempre se recomienda favorecer Task.Run
. Sin embargo, para permanecer dentro del modelo de ejecución de un único subproceso del grano, Task.Factory.StartNew
debe usarse. Entonces, ¿cómo usarlo correctamente? El peligro con Task.Factory.StartNew()
es su falta de soporte nativo para delegados asincrónicos. Esto significa que el código como var notIntendedTask = Task.Factory.StartNew(SomeDelegateAsync)
es probable que sea un error.
notIntendedTask
no se considera una tarea completada cuando finaliza. En su lugar, desencapsula siempre la tarea devuelta: var task = Task.Factory.StartNew(SomeDelegateAsync).Unwrap()
.
Ejemplo: Varias tareas y el programador de tareas
A continuación se muestra el código de ejemplo que ilustra el uso de TaskScheduler.Current
, Task.Run
y un programador personalizado especial para escapar del contexto de grano de Orleans y cómo volver a él.
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);
}
Ejemplo: Realizar una llamada a un grain desde código que se ejecuta en un hilo del grupo de subprocesos
Otro escenario implica la necesidad de "interrumpir" el modelo de programación de tareas del grano y ejecutarse en un hilo de un grupo de subprocesos (o en algún otro contexto fuera del grano), pero aún necesita llamar a otro grano. Las llamadas de grano se pueden realizar desde contextos que no son de grano sin tener que hacer nada enrevesado.
En el código siguiente se muestra cómo realizar una llamada a un grain desde el código que se ejecuta dentro de un grain, pero no en el contexto del grain.
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);
}
Trabajar con bibliotecas
Algunas bibliotecas externas usadas por el código pueden usar ConfigureAwait(false)
internamente. El uso ConfigureAwait(false)
de es una buena práctica y correcta en .NET al implementar bibliotecas de uso general. Esto no es un problema en Orleans. Siempre que el código de grano invocando el método de biblioteca espera la llamada de biblioteca con un valor normal await
, el código de grano es correcto. El resultado es exactamente el deseado: el código de la biblioteca ejecuta continuaciones en el planificador predeterminado (el valor devuelto por TaskScheduler.Default
, que no garantiza que las continuaciones se ejecuten en un subproceso ThreadPool, ya que a menudo se procesan en línea en el subproceso anterior), mientras que el código del grano se ejecuta en el planificador del grano.
Otra pregunta más frecuente es si las llamadas de biblioteca necesitan ejecutarse con Task.Run
; es decir, si el código de biblioteca necesita la descarga explícita en ThreadPool
(por ejemplo, await Task.Run(() => myLibrary.FooAsync())
). La respuesta es no. La descarga de código a ThreadPool
no es necesaria, excepto cuando el código de biblioteca realiza el bloqueo de llamadas sincrónicas. Normalmente, cualquier biblioteca asincrónica de .NET correcta y bien escrita (métodos que devuelven Task
y se denominan con un Async
sufijo) no realizan llamadas de bloqueo. Por lo tanto, no es necesario descargar nada en el ThreadPool
a menos que se sospeche que la biblioteca asincrónica tenga errores o que se use deliberadamente una biblioteca de bloqueo sincrónica.
Interbloqueos
Dado que los granos ejecutan un solo subproceso, es posible interbloquear un grano bloqueando sincrónicamente de una manera que requiere que varios subprocesos se desbloqueen. Esto significa que el código que llama a cualquiera de los siguientes métodos y propiedades puede bloquear un actor si las tareas proporcionadas no se han completado en el momento en que se invoca el método o la propiedad.
Task.Wait()
Task.Result
Task.WaitAny(...)
Task.WaitAll(...)
task.GetAwaiter().GetResult()
Evite estos métodos en cualquier servicio de alta simultaneidad porque pueden provocar un rendimiento deficiente y inestabilidad. Privan de recursos a .NET ThreadPool
bloqueando los subprocesos que podrían realizar un trabajo útil y necesitan que ThreadPool
inyecte hilos adicionales para su finalización. Al ejecutar código de grano, estos métodos pueden provocar que el grano se interbloquee, por lo tanto, deben evitarse también en código de grano.
Si algún trabajo de sincronización sobre asincronía es inevitable, es mejor mover ese trabajo a un programador independiente. La manera más sencilla es usar await Task.Run(() => task.Wait())
, por ejemplo. Tenga en cuenta que se recomienda encarecidamente evitar los procesos sincrónicos sobre asincrónicos, ya que daña la escalabilidad y el rendimiento de las aplicaciones.
Resumen: Gestión de tareas en Orleans
¿Qué está intentando hacer? | Cómo hacerlo |
---|---|
Ejecutar trabajo de segundo plano en subprocesos del grupo de subprocesos de .NET. No se permite ningún código de grano ni llamadas de grano. | Task.Run |
Ejecute una tarea de trabajo asincrónica desde código de grano con garantías de simultaneidad basadas en turnos de Orleans (consulte la información anterior). |
Task.Factory.StartNew(WorkerAsync).Unwrap() (Unwrap) |
Ejecute una tarea de trabajo sincrónica desde código de grano con garantías de simultaneidad basada en turnos de Orleans. | Task.Factory.StartNew(WorkerSync) |
Poner tiempos de espera para ejecutar elementos de trabajo | Task.Delay + Task.WhenAny |
Llamar a un método de biblioteca asincrónica |
await (esperar a) la llamada a la biblioteca |
Usar async /await |
Con el modelo de programación normal Task-Async de .NET. Es una opción con soporte y recomendada |
ConfigureAwait(false) |
No use esta opción en código de grano. Solo se permite en bibliotecas. |