Compartir a través de


Tareas externas y granos

Por diseño, todas las subtareas que se generan a partir de código de grano (por ejemplo, mediante await, ContinueWith o Task.Factory.StartNew) se envían en la misma TaskScheduler por activación que la tarea primaria y, por lo tanto, heredan el mismo modelo de ejecución uniproceso que tiene 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 de grano tenga que "separarse" del modelo de programación de tareas de Orleans y "hacer algo especial", como que Task apunte explícitamente a un programador de tareas diferente o a ThreadPool de .NET. Un ejemplo de un caso así es cuando el código de grano tiene que ejecutar una llamada de bloqueo remoto sincrónica (como una E/S remota). La ejecución de esa llamada de bloqueo en el contexto del grano bloqueará el grano y, por tanto, nunca se debe realizar. En vez de eso, el código de grano puede ejecutar este fragmento de código de bloqueo en el subproceso del grupo de subprocesos, combinar (await) la finalización de esa ejecución y continuar en el contexto del grano. Lo previsto es que obviar el programador de Orleans sea un escenario que solo se dé en usos muy avanzados, y apenas resulte necesario en los patrones de uso "normales".

API basadas en tareas

  1. await, TaskFactory.StartNew (consulte la información de abajo), Task.ContinueWith, Task.WhenAny, Task.WhenAll y Task.Delay respetan el programador de tareas actual. Esto significa que si se usan de manera predeterminada, sin utilizar un TaskScheduler diferente, se ejecutarán en el contexto del grano.

  2. Tanto Task.Run como el delegado endMethod de TaskFactory.FromAsyncno respetan el programador de tareas actual. Ambos usan el programador TaskScheduler.Default, que es el programador de tareas del grupo de subprocesos de .NET. Por lo tanto, el código dentro de Task.Run y endMethod en Task.Factory.FromAsync se ejecutarán siempre en el grupo de subprocesos de .NET, sin usar el modelo de ejecución de subproceso único de los granos de Orleans. Sin embargo, cualquier código después de await Task.Run o await Task.Factory.FromAsync se volverá a ejecutar en el programador en el momento en que se creó la tarea, que pertenece al programador del grano.

  3. Task.ConfigureAwait con false es una API explícita para obviar al programador de tareas actual. Hará que el código que haya después de una tarea esperada se ejecute en el programador TaskScheduler.Default, que es el grupo de subprocesos de .NET, y, por tanto, interrumpirá la ejecución uniproceso del grano.

    Precaución

    En general, nunca debe usar ConfigureAwait(false) directamente en código de granos.

  4. 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. Los métodos async void pueden bloquear inmediatamente el proceso actual si permiten que se escape una excepción, sin que haya forma de controlarla. Esto también es cierto para List<T>.ForEach(async element => ...) y para cualquier otro método que acepte una Action<T>, ya que el delegado asincrónico se convertirá obligatoriamente en un delegado async void.

Delegados Task.Factory.StartNew y async

La recomendación habitual para programar tareas en cualquier programa de C# es priorizar el uso de Task.Run por encima de Task.Factory.StartNew. Una búsqueda rápida en Google sobre el uso de Task.Factory.StartNew sugerirá que es peligrosa y que siempre debe priorizarse Task.Run. Pero si queremos permanecer en el modelo de ejecución uniproceso del grano para usarlo con nuestro grano, necesitamos usar esa instrucción, así que... ¿Cómo lo hacemos correctamente? El peligro al usar Task.Factory.StartNew() es que no admite delegados asincrónicos de forma nativa. Esto indica que es probable que se trate de un error: var notIntendedTask = Task.Factory.StartNew(SomeDelegateAsync). notIntendedTaskno es una tarea que se complete al completarse SomeDelegateAsync. En su lugar, hay que desajustar siempre la tarea devuelta: var task = Task.Factory.StartNew(SomeDelegateAsync).Unwrap().

Ejemplo de varias tareas y del programador de tareas

A continuación se muestra código de ejemplo que ilustra el uso de TaskScheduler.Current, Task.Run y un programador personalizado especial para salir del contexto de granos 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 de realización de una llamada de grano desde un código que se ejecuta en un grupo de subprocesos

Otro escenario es un fragmento de código de grano que necesita "separarse" del modelo de programación de tareas del grano y ejecutarse en un grupo de subprocesos (o en otro contexto que no sea de granos), 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.

Este es el código que muestra cómo se puede realizar una llamada de grano desde un fragmento de código que se ejecuta dentro de un grano, pero no en el contexto del grano.

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

Es posible que algunas bibliotecas externas que use el código utilicen ConfigureAwait(false) internamente. En .NET, el uso de ConfigureAwait(false) es una práctica deseable y correcta al implementar bibliotecas de uso general. Esto no supone un problema en Orleans. Siempre que el código del grano que invoca el método de biblioteca esté esperando la llamada de biblioteca con el típico await, el código de grano es correcto. El resultado será exactamente el deseado: el código de biblioteca ejecutará continuaciones en el programador predeterminado (el valor devuelto por TaskScheduler.Default, que no garantiza que las continuaciones se ejecuten en un subproceso ThreadPool, ya que las continuaciones a menudo se insertan en el subproceso anterior), mientras que el código de grano se ejecutará en el programador del grano.

Otra pregunta frecuente es si es necesario ejecutar llamadas de biblioteca con Task.Run; es decir, si es necesario descargar explícitamente el código de biblioteca en ThreadPool (para que el código de grano haga el proceso Task.Run(() => myLibrary.FooAsync())). La respuesta es no. No es necesario descargar ningún código en ThreadPool excepto en el caso del código de biblioteca que realiza llamadas sincrónicas de bloqueo. Normalmente, cualquier biblioteca asincrónica de .NET correcta y bien escrita (con métodos que devuelven Task y van denominados con un sufijo Async) no realiza llamadas de bloqueo. Por lo tanto, no es necesario descargar nada en ThreadPool, a menos que sospeche que la biblioteca asincrónica tiene errores o si está usando deliberadamente una biblioteca de bloqueo sincrónica.

Interbloqueos

Dado que los granos se ejecutan en un único proceso, es posible interbloquear un grano mediante un bloqueo sincrónico, de tal forma que serían necesarios varios subprocesos para su desbloqueo. Esto significa que el código que llama a cualquiera de los métodos y propiedades siguientes puede interbloquear un grano si las tareas proporcionadas aún 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()

Estos métodos deben evitarse en cualquier servicio de alta simultaneidad, ya que pueden provocar inestabilidad y un rendimiento deficiente al llevar a la inanición de ThreadPool de .NET. Esto ocurre porque esos métodos producen el bloqueo de subprocesos que podrían realizar un trabajo útil y la necesidad de que ThreadPool de .NET inserte subprocesos adicionales para que estos puedan completarse. Al ejecutar código de grano, estos métodos, como se mencionó anteriormente, pueden provocar que el grano interbloquee y, por lo tanto, también deben evitarse en el código de grano.

Si hay algún trabajo sync-over-async que no se puede evitar, es mejor trasladar ese trabajo a un programador independiente. La manera más sencilla de hacerlo es usar await Task.Run(() => task.Wait()) como ejemplo. Tenga en cuenta que se recomienda encarecidamente evitar los trabajos sync-over-async, ya que, como se mencionó anteriormente, causarán estragos en la escalabilidad y en el rendimiento de la aplicación.

Resumen del trabajo con 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.