Tarea vs diferencias de hilo

Soy nuevo en la progtwigción paralela. Hay dos clases disponibles en .NET: Task e Thread .

Entonces, la pregunta es: ¿Cuál es la diferencia entre esas clases? ¿Cuándo es mejor usar Thread y cuándo Task ?

Thread es un concepto de nivel inferior: si está iniciando directamente un subproceso, sabe que será un subproceso independiente, en lugar de ejecutarse en el grupo de subprocesos, etc.

Task embargo, la Task es más que una abstracción de “dónde ejecutar código”: en realidad es solo “la promesa de un resultado en el futuro”. Entonces como algunos ejemplos diferentes:

  • Task.Delay no necesita ningún tiempo de CPU real; es como configurar un temporizador para que se dispare en el futuro
  • Una tarea devuelta por WebClient.DownloadStringTaskAsync no requerirá mucho tiempo de CPU localmente; representa un resultado que probablemente pase la mayor parte del tiempo en latencia de red o trabajo remoto (en el servidor web)
  • Una tarea devuelta por Task.Run() realmente dice “Quiero que ejecutes este código por separado”; el hilo exacto en el que se ejecuta ese código depende de una serie de factores.

Tenga en cuenta que la abstracción Task es fundamental para el soporte async en C # 5.

En general, te recomiendo que uses la abstracción de nivel superior siempre que puedas: en el código C # moderno, rara vez necesitarás iniciar explícitamente tu propio hilo.

Fuente

Hilo

El subproceso representa un subproceso de nivel de sistema operativo real, con su propia stack y recursos de kernel. (técnicamente, una implementación de CLR podría usar fibras en su lugar, pero ningún CLR existente lo hace). El subproceso permite el mayor grado de control; puede Abortar () o Suspender () o Reanudar () un hilo (aunque esta es una muy mala idea), puede observar su estado y puede establecer propiedades de nivel de subproceso como el tamaño de stack, el estado del departamento o la cultura.

El problema con el subproceso es que los subprocesos del sistema operativo son costosos. Cada subproceso que ha consumido una cantidad no trivial de memoria para su stack, y agrega sobrecarga de CPU adicional como el contexto del procesador: cambiar entre subprocesos. En cambio, es mejor tener un pequeño grupo de subprocesos para ejecutar su código cuando el trabajo esté disponible.

Hay momentos en que no hay ningún hilo alternativo. Si necesita especificar el nombre (para fines de depuración) o el estado del departamento (para mostrar una IU), debe crear su propio Tema (tenga en cuenta que tener varios hilos de UI generalmente es una mala idea). Además, si desea mantener un objeto que sea propiedad de un único subproceso y solo pueda ser utilizado por ese subproceso, es mucho más fácil crear explícitamente una instancia de Subproceso para que pueda verificar fácilmente si el código que intenta usarlo se está ejecutando. en el hilo correcto

ThreadPool

ThreadPool es un contenedor de un conjunto de hilos mantenido por el CLR. ThreadPool no te da ningún control; puede enviar trabajo para ejecutar en algún momento, y puede controlar el tamaño del grupo, pero no puede establecer nada más. Ni siquiera puede saber cuándo el grupo comenzará a ejecutar el trabajo que le envía.

Usar ThreadPool evita la sobrecarga de crear demasiados hilos. Sin embargo, si envía demasiadas tareas de larga ejecución al grupo de subprocesos, puede llenarse y el trabajo posterior que envíe puede terminar esperando a que los elementos anteriores de ejecución larga finalicen. Además, ThreadPool no ofrece forma de averiguar cuándo se ha completado un elemento de trabajo (a diferencia de Thread.Join ()), ni una forma de obtener el resultado. Por lo tanto, ThreadPool se usa mejor para operaciones cortas donde la persona que llama no necesita el resultado.

Tarea

Finalmente, la clase Task de la Task Parallel Library ofrece lo mejor de ambos mundos. Al igual que ThreadPool, una tarea no crea su propio hilo del sistema operativo. En cambio, las tareas las ejecuta TaskScheduler; el planificador predeterminado simplemente se ejecuta en ThreadPool.

A diferencia de ThreadPool, Task también le permite saber cuándo termina y (a través de la Tarea genérica) devolver un resultado. Puede llamar a ContinueWith () en una Tarea existente para que ejecute más código una vez que la tarea finalice (si ya ha finalizado, ejecutará la callback inmediatamente). Si la tarea es genérica, ContinueWith () le pasará el resultado de la tarea, lo que le permite ejecutar más código que la use.

También puede esperar sincrónicamente a que finalice una tarea llamando a Wait () (o, para una tarea genérica, obteniendo la propiedad Result). Al igual que Thread.Join (), esto bloqueará el hilo de llamada hasta que la tarea finalice. La espera sincrónica de una tarea suele ser mala idea; impide que el hilo de llamada realice otro trabajo y también puede provocar interlockings si la tarea termina esperando (incluso de forma asíncrona) para el hilo actual.

Dado que las tareas aún se ejecutan en ThreadPool, no se deben usar para operaciones de larga ejecución, ya que aún pueden llenar el grupo de subprocesos y bloquear el nuevo trabajo. En cambio, Task proporciona una opción LongRunning, que le indicará a TaskScheduler que cree un nuevo hilo en lugar de ejecutarlo en ThreadPool.

Todas las API de concurrencia de alto nivel más nuevas, incluidos los métodos Parallel.For * (), PLINQ, C # 5 en espera, y los métodos asincrónicos modernos en BCL, se basan en Tarea.

Conclusión

La conclusión es que Tarea es casi siempre la mejor opción; proporciona una API mucho más potente y evita el desperdicio de los hilos del sistema operativo.

Las únicas razones para crear explícitamente sus propios subprocesos en el código moderno son configurar las opciones por subproceso o mantener un subproceso persistente que necesita mantener su propia identidad.

La tarea es un concepto de nivel superior al hilo … y eso es lo que significa esta frase:

  1. No puede usar Abort / ThreadAbortedException, debe admitir cancelar evento en su “código de negocio”, prueba periódicamente el indicador token.IsCancellationRequested (también evite las conexiones largas o sin tiempo, por ejemplo, a db, de lo contrario, nunca tendrá la oportunidad de probar este indicador). Por el mismo motivo, Thread.Sleep(delay) debe reemplazarse con Task.Delay(delay, token);

  2. No hay funcionalidad de métodos Suspend () y Resume () de thread con tareas. La instancia de la tarea tampoco se puede reutilizar.

  3. Pero obtienes dos herramientas nuevas: continuaciones y tareas anidadas / secundarias ; esas dos muestras demuestran la idea y la syntax:

      // continuation - execute the delegate, when all tasks[] had been finished Task.Factory.ContinueWhenAll( tasks, () => { int answer = tasks[0].Result + tasks[1].Result; Console.WriteLine("The answer is {0}", answer); } ); //StartNew - starts task immediately, parent ends whith child var parent = Task.Factory.StartNew (() => { var child = Task.Factory.StartNew(() => { //... }); }, TaskCreationOptions.AttachedToParent ); 
  4. Por lo tanto, el hilo del sistema está completamente oculto de la tarea, pero el código de la tarea se ejecuta en el hilo concreto del sistema. Los subprocesos del sistema son recursos para las tareas y, por supuesto, todavía hay un grupo de subprocesos bajo el capó de la ejecución paralela de tareas. Puede haber diferentes estrategias para ejecutar tareas nuevas. Otro recurso compartido TaskScheduler se preocupa por esto. Algunos problemas que TaskScheduler resuelve 1) prefieren ejecutar tareas y su ubicación en el mismo subproceso minimizando el costo de conmutación, también conocido como ejecución en línea . 2) prefieren ejecutar tareas en un orden en el que se iniciaron – aka PreferFairness 3) distribución más efectiva de tareas entre subprocesos inactivos dependiendo del “conocimiento previo de la actividad de las tareas”, también conocido como robo de trabajo . Importante: en general, “asincrónico” no es lo mismo que “paralelo”. Al jugar con las opciones de TaskScheduler puede configurar tareas asíncronas para que se ejecuten en un hilo de forma síncrona.

  5. Las tareas se integran con las características de C # async / requestButton.Clicked += async (o, e) => ProcessResponce(await client.RequestAsync(e.ResourceName)); también requestButton.Clicked += async (o, e) => ProcessResponce(await client.RequestAsync(e.ResourceName)); como Promise Model , por ejemplo, requestButton.Clicked += async (o, e) => ProcessResponce(await client.RequestAsync(e.ResourceName)); la ejecución de client.RequestAsync no bloqueará el subproceso de UI. Importante: bajo el capó La llamada de delegado Clicked es absolutamente regular (todo el enrutamiento lo hace el comstackdor).

Eso es suficiente para hacer una elección. Si necesita admitir la funcionalidad Cancelar del hilo que tiende a bloquearse (por ejemplo, conexión timeoutless), o si está creando cálculos de fondo de subprocesos múltiples y desea administrar subprocesos utilizando Suspender / Reanudar, eso significa administrar la ejecución paralela manualmente – permanecer con Subproceso. De lo contrario, vaya a Tareas porque le facilitarán la manipulación en grupos de ellos y están integrados en el lenguaje.

La clase Thread se usa para crear y manipular un hilo en Windows.

Una Task representa alguna operación asíncrona y es parte de la Biblioteca de tareas paralelas , un conjunto de API para ejecutar tareas de forma asíncrona y en paralelo.

En los días de antaño (es decir, antes de TPL) solía ser que el uso de la clase Thread era una de las formas estándar de ejecutar código en segundo plano o en paralelo (una mejor alternativa era utilizar un ThreadPool ), sin embargo, esto era engorroso. y tenía varias desventajas, una de las cuales era la sobrecarga de rendimiento de crear un hilo completamente nuevo para realizar una tarea en segundo plano.

Hoy en día el uso de tareas y el TPL es una solución mucho mejor el 90% del tiempo, ya que proporciona abstracciones que permiten un uso mucho más eficiente de los recursos del sistema. Me imagino que hay algunos escenarios en los que desea un control explícito sobre el hilo en el que está ejecutando su código, sin embargo, en términos generales, si desea ejecutar algo de manera asincrónica, su primer puerto de escala debería ser el TPL.