Es Tarea.Fábrica.StartNew () ¿se garantiza que usará otro hilo que el hilo que llama?


Estoy comenzando una nueva tarea desde una función pero no me gustaría que se ejecute en el mismo hilo. No me importa en qué hilo se ejecuta mientras sea diferente (por lo que la información dada en esta pregunta no ayuda).

¿Estoy garantizado que el siguiente código siempre saldrá TestLock antes de permitir que Task t lo vuelva a ingresar? Si no, ¿ cuál es el patrón de diseño recomendado para evitar la reentrencia?

object TestLock = new object();

public void Test(bool stop = false) {
    Task t;
    lock (this.TestLock) {
        if (stop) return;
        t = Task.Factory.StartNew(() => { this.Test(stop: true); });
    }
    t.Wait();
}

Edit: Basado en la siguiente respuesta de Jon Skeet y Stephen Toub, una forma sencilla de prevenir deterministamente la reentrada sería pasar un CancellationToken, como se ilustra en este método de extensión:

public static Task StartNewOnDifferentThread(this TaskFactory taskFactory, Action action) 
 {
    return taskFactory.StartNew(action: action, cancellationToken: new CancellationToken());
}
Author: Community, 2012-09-03

4 answers

Envié un correo a Stephen Toub - un miembro del Equipo PFX - sobre esta pregunta. Ha vuelto a mí muy rápido, con muchos detalles, así que simplemente copiaré y pegaré su texto aquí. No lo he citado todo, ya que leer una gran cantidad de texto citado termina siendo menos cómodo que vanilla black-on-white, pero en realidad, este es Stephen-No se muchas cosas :) He hecho este wiki de la comunidad de respuestas para reflejar que toda la bondad a continuación no es realmente mi contenido:

Si llama Wait() en una Tarea que se haya completado, no habrá ningún bloqueo (solo lanzará una excepción si la tarea se completó con un TaskStatus que no sea RanToCompletion, o regresará como un nop). Si llamas a Wait() en una Tarea que ya se está ejecutando, debe bloquearse, ya que no hay nada más que pueda hacer razonablemente (cuando digo bloquear, estoy incluyendo tanto la espera basada en el núcleo como la rotación, ya que normalmente hará una mezcla de ambos). Del mismo modo, si llamas a Wait() en una Tarea que tiene el estado Created o WaitingForActivation, se bloqueará hasta que la tarea se haya completado. Ninguno de esos es el caso interesante que se está discutiendo.

El caso interesante es cuando llamas a Wait() en una Tarea en el estado WaitingToRun, lo que significa que previamente se ha puesto en cola a un TaskScheduler pero ese TaskScheduler aún no ha logrado ejecutar realmente el delegado de la Tarea. En ese caso, la llamada a Wait pedirá la scheduler si está bien ejecutar la Tarea en ese momento en el subproceso actual, a través de una llamada al método TryExecuteTaskInline del scheduler. Esto se llama inlining. El scheduler puede elegir entre insertar la tarea a través de una llamada a base.TryExecuteTask, o puede devolver 'false' para indicar que no está ejecutando la tarea (a menudo esto se hace con logic like...

return SomeSchedulerSpecificCondition() ? false : TryExecuteTask(task);

La razón por la que TryExecuteTask devuelve un booleano es que maneja la sincronización para garantizar que una tarea dada solo se ejecute una vez). Por lo tanto, si un scheduler quiere prohibir completamente la inserción de la Tarea durante Wait, solo se puede implementar como return false; Si un scheduler quiere siempre permitir la inserción siempre que sea posible, solo se puede implementar como:

return TryExecuteTask(task);

En la implementación actual (tanto. NET 4 como. NET 4.5, y personalmente no espero que esto cambie), el scheduler predeterminado que se dirige al ThreadPool permite la inserción si el subproceso actual es un subproceso ThreadPool y si ese subproceso fue el que haber puesto previamente la tarea en cola.

Tenga en cuenta que no hay reentrada arbitraria aquí, ya que el programador predeterminado no bombeará hilos arbitrarios cuando espere una tarea... solo permitirá que esa tarea se alinee, y por supuesto cualquier inserción de esa tarea a su vez decide hacerlo. También tenga en cuenta que Wait ni siquiera preguntará al programador en ciertas condiciones, prefiriendo bloquear. Por ejemplo, si pasa un cancelable CancellationToken , o si pasa un no infinito timeout, no intentará inline porque podría tomar una cantidad de tiempo arbitrariamente larga para inline la ejecución de la tarea, que es todo o nada, y que podría terminar retrasando significativamente la solicitud de cancelación o tiempo de espera. En general, TPL intenta encontrar un equilibrio decente aquí entre desperdiciar el hilo que está haciendo el Wait'ing y reutilizar ese hilo por demasiado. Este tipo de inserción es realmente importante para los problemas recursivos de divide y vencerás (por ejemplo, QuickSort) donde genera múltiples tareas y luego espera a que se completen. Si esto se hiciera sin incrustar, se bloquearía muy rápidamente a medida que se agotan todos los hilos de la piscina y los futuros que quería darle.

Aparte de Wait, también es (remotamente) posible que la Tarea .Fábrica.StartNew call podría terminar ejecutando la tarea entonces y allí, si el scheduler que se está utilizando elige ejecutar la tarea sincrónicamente como parte de la llamada QueueTask. Ninguno de los schedulers construido en .NETA nunca va a hacer esto, y yo personalmente creo que sería un mal diseño para el programador, pero es teóricamente posible, por ejemplo:

protected override void QueueTask(Task task, bool wasPreviouslyQueued)
{
    return TryExecuteTask(task);
}

La sobrecarga de Task.Factory.StartNew que no acepta un TaskScheduler utiliza el programador de la TaskFactory, que en el caso de Task.Factory objetivos TaskScheduler.Current. Esto significa que si llama a Task.Factory.StartNew desde una Tarea en cola a este mítico RunSynchronouslyTaskScheduler, también haría cola a RunSynchronouslyTaskScheduler, lo que resulta en que la llamada StartNew ejecute la tarea de forma sincrónica. Si estás preocupado acerca de esto (por ejemplo, estás implementando una biblioteca y no sabes desde dónde vas a ser llamado), puedes pasar explícitamente TaskScheduler.Default a la llamada StartNew, usar Task.Run (que siempre va a TaskScheduler.Default), o usar un TaskFactory creado para destino TaskScheduler.Default.


EDITAR: Bien, parece que estaba completamente equivocado, y un hilo que actualmente está esperando una tarea puede ser secuestrado. He aquí un ejemplo más simple de esto:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1 {
    class Program {
        static void Main() {
            for (int i = 0; i < 10; i++)
            {
                Task.Factory.StartNew(Launch).Wait();
            }
        }

        static void Launch()
        {
            Console.WriteLine("Launch thread: {0}", 
                              Thread.CurrentThread.ManagedThreadId);
            Task.Factory.StartNew(Nested).Wait();
        }

        static void Nested()
        {
            Console.WriteLine("Nested thread: {0}", 
                              Thread.CurrentThread.ManagedThreadId);
        }
    }
}

Salida de muestra:

Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4

As se puede ver, hay muchas veces cuando el hilo de espera se reutiliza para ejecutar la nueva tarea. Esto puede suceder incluso si el hilo ha adquirido un bloqueo. Reingreso desagradable. Estoy convenientemente sorprendido y preocupado: (

 82
Author: Jon Skeet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-25 19:50:25

¿Por qué no simplemente diseñar para ello, en lugar de hacer lo imposible para asegurarse de que no suceda?

El TPL es una pista falsa aquí, la reentrada puede ocurrir en cualquier código siempre que pueda crear un ciclo, y no sabe con certeza qué va a suceder 'al sur' de su marco de pila. La reentrada síncrona es el mejor resultado aquí , al menos no puedes bloquearte a ti mismo (con la misma facilidad).

Los bloqueos gestionan la sincronización de subprocesos cruzados. Son ortogonales a la gestión de la reentrada. Excepto está protegiendo un recurso genuino de un solo uso (probablemente un dispositivo físico, en cuyo caso probablemente debería usar una cola), por qué no solo asegurarse de que el estado de su instancia sea consistente para que la reentrada pueda "funcionar".

(Pensamiento secundario: ¿están los semáforos reentrando sin decrementarse?)

 4
Author: piers7,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-09-04 13:22:33

Puede probar esto fácilmente escribiendo una aplicación rápida que comparta una conexión de socket entre subprocesos / tareas.

La tarea adquiriría un bloqueo antes de enviar un mensaje por el socket y esperar una respuesta. Una vez que esto se bloquea y se vuelve inactivo (IOBlock) establezca otra tarea en el mismo bloque para hacer lo mismo. Debe bloquear al adquirir el bloqueo, si no lo hace y la segunda tarea se le permite pasar el bloqueo porque se ejecuta por el mismo hilo, entonces tiene un problema.

 0
Author: JonPen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-09-26 20:58:30

Solución con new CancellationToken() propuesto por Erwin no funcionó para mí, en línea pasó a ocurrir de todos modos.

Así que terminé usando otra condición aconsejada por Jon y Stephen (... or if you pass in a non-infinite timeout ...):

  Task<TResult> task = Task.Run(func);
  task.Wait(TimeSpan.FromHours(1)); // Whatever is enough for task to start
  return task.Result;

Nota: Omitiendo el manejo de excepciones, etc. aquí para simplificar, debe tener en cuenta los que están en el código de producción.

 0
Author: Vitaliy Ulantikov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-09-01 07:09:58