Compartir a través de


Tareas externas y granos

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

  1. 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.

  2. Tanto Task.Run como el delegado endMethod de TaskFactory.FromAsyncno respetan el programador de tareas actual. Ambos usan el TaskScheduler.Default planificador, el planificador de tareas del grupo de subprocesos de .NET. Por lo tanto, el código dentro de Task.Run y en endMethodTask.Factory.FromAsyncsiempre 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 de await Task.Run o await 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.

  3. 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 espere Task 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.

  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. Un async 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 a List<T>.ForEach(async element => ...) y a cualquier otro método que acepte Action<T>, ya que el delegado asincrónico se adapta a un delegado async 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.