Publico el artículo original sobre Habr, cuya traducción se publica en el blog Codingsight .La segunda parte está disponible aquí.La necesidad de hacer algo de forma asincrónica, sin esperar el resultado aquí y ahora, o compartir mucho trabajo entre varias unidades que lo realizaban, fue incluso antes del advenimiento de las computadoras. Con su apariencia, tal necesidad se ha vuelto muy tangible. Ahora, en 2019, escribiendo este artículo en una computadora portátil con un procesador Intel Core de 8 núcleos, en el que no funcionan cien procesos al mismo tiempo, sino incluso más subprocesos. Junto a él se encuentra un teléfono ligeramente maltratado, comprado hace un par de años, con un procesador de 8 núcleos a bordo. Los recursos temáticos están llenos de artículos y videos donde sus autores admiran los teléfonos inteligentes emblemáticos de este año donde colocan procesadores de 16 núcleos. Por menos de $ 20 / hora, MS Azure proporciona una máquina virtual con 128 procesadores centrales y 2 TB de RAM. Desafortunadamente, es imposible maximizar y frenar este poder sin poder controlar la interacción de los flujos.
Terminología
Proceso : un objeto del sistema operativo, un espacio de direcciones aislado, contiene subprocesos.
Thread (Thread) : un objeto del sistema operativo, la unidad de ejecución más pequeña, parte de un proceso, los hilos comparten memoria y otros recursos entre sí dentro del proceso.
La multitarea es una característica del sistema operativo, la capacidad de ejecutar múltiples procesos al mismo tiempo
Multinúcleo : una propiedad del procesador, la capacidad de usar múltiples núcleos para el procesamiento de datos
Multiprocesamiento : una propiedad de una computadora, la capacidad de trabajar simultáneamente con múltiples procesadores físicamente
El subprocesamiento múltiple es una propiedad de un proceso, la capacidad de distribuir el procesamiento de datos entre múltiples subprocesos.
Paralelismo : realizar varias acciones físicamente al mismo tiempo por unidad de tiempo
Asincronía : la ejecución de una operación sin esperar el final de este procesamiento, el resultado de la ejecución se puede procesar más adelante.
Metáfora
No todas las definiciones son buenas y algunas necesitan una explicación adicional, por lo que agregaré una metáfora para cocinar el desayuno a la terminología introducida formalmente. Preparar el desayuno en esta metáfora es un proceso.
Preparando el desayuno en la mañana I (
CPU ) vengo a la cocina (
Computadora ). Tengo 2 manos (
núcleos ). La cocina tiene varios dispositivos (
IO ): horno, tetera, tostadora, refrigerador. Enciendo el gas, pongo una sartén y vierto el aceite allí, sin esperar hasta que se caliente (
asincrónicamente, sin bloqueo, IO-Wait ), saco los huevos del refrigerador y los rompo en un plato, y luego los golpeo con una mano (
Hilo # 1 ), y el segundo (
Hilo # 2 ) sostengo la placa (Recurso compartido). Ahora todavía encendería la tetera, pero no hay suficientes manos (
Hambre de hambre ) Durante este tiempo, la sartén se calienta (Procesando el resultado) donde vierto lo que batí. Alcanzo la tetera, la enciendo y observo estúpidamente cómo hierve el agua (
Bloqueo-IO-Espera ), aunque podría lavar el plato durante este tiempo, donde batí la tortilla.
Cociné una tortilla usando solo 2 manos, y no tengo más, pero al mismo tiempo, 3 operaciones tuvieron lugar en el momento de batir una tortilla: batir una tortilla, sostener un plato, calentar una sartén. La CPU es la parte más rápida de la computadora, IO es eso con más frecuencia ralentiza todo, por lo que a menudo una solución efectiva es tomar algo de CPU mientras recibe datos de IO.
Continuando con la metáfora:
- Si en el proceso de preparar una tortilla, también intentara cambiarme de ropa, este sería un ejemplo de multitarea. Un matiz importante: las computadoras con esto son mucho mejores que las personas.
- Una cocina con varios chefs, por ejemplo en un restaurante, es una computadora de varios núcleos.
- Muchos restaurantes de patio de comidas en un centro comercial - centro de datos
Herramientas .NET
Al trabajar con subprocesos, como en muchas otras cosas, .NET es bueno. Con cada nueva versión, presenta cada vez más herramientas nuevas para trabajar con ellas, nuevas capas de abstracción sobre los hilos del sistema operativo. Al trabajar con la construcción de abstracciones, los desarrolladores de framework usan el enfoque que deja la posibilidad cuando se usa abstracción de alto nivel, bajará uno o varios niveles por debajo. En la mayoría de los casos, esto no es necesario, además, esto abre la posibilidad de que se dispare una escopeta en el pie, pero a veces, en casos excepcionales, esta puede ser la única forma de resolver un problema que no se resuelve en el nivel actual de abstracción.
Por herramientas, me refiero a las interfaces de programa (API) proporcionadas por el marco y los paquetes de terceros, y a una solución de software completa que simplifica la búsqueda de cualquier problema asociado con el código de subprocesos múltiples.
Inicio de transmisión
La clase Thread, la clase más básica en .NET para trabajar con hilos. El constructor acepta uno de los dos delegados:
- ThreadStart: sin parámetros
- ParametrizedThreadStart: con un parámetro de tipo objeto.
El delegado se ejecutará en el subproceso recién creado después de llamar al método Start, si un delegado del tipo ParametrizedThreadStart se pasó al constructor, se debe pasar un objeto al método Start. Este mecanismo es necesario para transferir cualquier información local a la secuencia. Vale la pena señalar que crear una secuencia es una operación costosa, y la secuencia en sí misma es un objeto pesado, al menos porque se asigna 1 MB de memoria a la pila, y requiere interacción con la API del sistema operativo.
new Thread(...).Start(...);
La clase ThreadPool representa el concepto de un grupo. En .NET, el grupo de subprocesos es una obra de arte y los desarrolladores de Microsoft se han esforzado mucho para que funcione de manera óptima en una amplia variedad de escenarios.
Concepto general:Desde el comienzo, la aplicación en segundo plano crea varios subprocesos en reserva y brinda la oportunidad de ponerlos en uso. Si los hilos se usan con frecuencia y en grandes cantidades, el grupo se expande para satisfacer la necesidad del código de llamada. Cuando no hay flujos libres en la agrupación en el momento adecuado, esperará a que regrese uno de los flujos o creará uno nuevo. De ello se deduce que el grupo de subprocesos es ideal para algunas acciones cortas y poco adecuado para operaciones que operan como un servicio en toda la aplicación.
Para usar un subproceso del grupo, hay un método QueueUserWorkItem que acepta un delegado de tipo WaitCallback, que es la misma firma que ParametrizedThreadStart, y el parámetro que se le pasa realiza la misma función. ThreadPool.QueueUserWorkItem(...);
El método de grupo de subprocesos menos conocido RegisterWaitForSingleObject se utiliza para organizar operaciones de E / S sin bloqueo. El delegado pasado a este método se llamará cuando WaitHandle pasado al método esté "Liberado".
ThreadPool.RegisterWaitForSingleObject(...)
.NET tiene un temporizador de flujo y se diferencia de los temporizadores WinForms / WPF en que su controlador se llamará en un flujo tomado del grupo.
System.Threading.Timer
También hay una forma bastante exótica de enviar un delegado al hilo desde el grupo: el método BeginInvoke.
DelegateInstance.BeginInvoke
También quiero detenerme en transmitir una función que llama a muchos de los métodos anteriores: CreateThread desde Kernel32.dll Win32 API. Hay una manera, gracias al mecanismo de métodos externos, de llamar a esta función. Vi tal desafío solo una vez en un terrible ejemplo de código heredado, y la motivación del autor para hacer eso sigue siendo un misterio para mí.
Kernel32.dll CreateThread
Ver y depurar hilos
Los hilos que creó personalmente por todos los componentes de terceros y el grupo .NET se pueden ver en la ventana Threads de Visual Studio. Esta ventana mostrará información sobre los flujos solo cuando la aplicación esté en depuración y en modo de interrupción (modo de interrupción). Aquí puede ver convenientemente los nombres de pila y las prioridades de cada subproceso, cambiar la depuración a un subproceso específico. La propiedad Prioridad de la clase Thread le permite establecer la prioridad del hilo, que OC y CLR percibirán como una recomendación al dividir el tiempo de CPU entre los hilos.

Biblioteca paralela de tareas
La biblioteca paralela de tareas (TPL) apareció en .NET 4.0. Ahora es el estándar y la herramienta principal para trabajar con asincronía. Cualquier código que use un enfoque anterior se considera heredado. La unidad básica de TPL es la clase Task del espacio de nombres System.Threading.Tasks. La tarea es una abstracción sobre un hilo. Con la nueva versión de C #, obtuvimos una forma elegante de trabajar con Task: operadores asíncronos / en espera. Estos conceptos hicieron posible escribir código asincrónico como si fuera simple y sincrónico, esto hizo posible que incluso las personas con poca comprensión de la cocina interna de hilos escriban aplicaciones que los usan, aplicaciones que no se congelan durante operaciones largas. El uso de async / await es un tema para uno o incluso varios artículos, pero intentaré obtener la esencia de algunas oraciones:
- async es un modificador del método que devuelve Task o void
- y esperar es la declaración de espera sin bloqueo de la tarea.
Una vez más: el operador de espera, en el caso general (hay excepciones), liberará aún más el hilo de ejecución actual, y cuando la Tarea termine su ejecución, y el hilo (de hecho es más correcto decir el contexto, pero más sobre eso más adelante) será libre de continuar el método. Dentro de .NET, este mecanismo se implementa de la misma manera que el rendimiento de rendimiento, cuando un método escrito se convierte en una clase completa, que es una máquina de estados y puede ejecutarse en partes separadas dependiendo de estos estados. Cualquier persona interesada puede escribir cualquier código simple usando asyn / await, compilar y ver el ensamblaje usando JetBrains dotPeek con el código generado por el compilador habilitado.
Considere las opciones para iniciar y usar Tarea. Usando el siguiente ejemplo de código, creamos una nueva tarea que no hace nada útil (
Thread.Sleep (10000) ), pero en la vida real debería ser algún tipo de trabajo complejo que involucra CPU.
using TCO = System.Threading.Tasks.TaskCreationOptions; public static async void VoidAsyncMethod() { var cancellationSource = new CancellationTokenSource(); await Task.Factory.StartNew(
La tarea se crea con una serie de opciones:
- LongRunning es una pista de que la tarea no se completará rápidamente, lo que significa que puede valer la pena considerar no tomar un hilo del grupo, sino crear uno separado para esta Tarea para no dañar a los demás.
- AttachedToParent: las tareas se pueden organizar en una jerarquía. Si se utilizó esta opción, la Tarea puede estar en un estado cuando se ha completado y está esperando a que se completen los niños.
- PreferFairness: significa que sería bueno ejecutar las tareas enviadas antes para su ejecución antes de las que se enviaron más tarde. Pero esto es solo una recomendación y el resultado no está garantizado.
El segundo parámetro para el método pasó CancellationToken. Para procesar correctamente la cancelación de una operación después de su lanzamiento, el código ejecutado debe completarse con verificaciones de estado de CancellationToken. Si no hay comprobaciones, el método de cancelación invocado en el objeto CancellationTokenSource podrá detener la ejecución de la tarea solo antes de que comience.
El último parámetro pasó el objeto del planificador de tipo TaskScheduler. Esta clase y sus descendientes están diseñados para controlar las estrategias para distribuir Task'ov por subproceso, de forma predeterminada, la Tarea se ejecutará en un subproceso aleatorio del grupo.
El operador de espera se aplica a la Tarea creada, lo que significa que el código escrito después, si lo hay, se ejecutará en el mismo contexto (a menudo esto significa que está en el mismo hilo) que el código antes de esperar.
El método está marcado como vacío asíncrono, lo que significa que puede usar el operador de espera en él, pero el código de llamada no puede esperar a la ejecución. Si esta característica es necesaria, entonces el método debería devolver la Tarea. Los métodos marcados como nulo asíncrono son bastante comunes: por regla general, estos son controladores de eventos u otros métodos que funcionan según el principio de disparar y olvidar. Si necesita no solo dar la oportunidad de esperar hasta la finalización de la ejecución, sino también devolver el resultado, debe usar Tarea.
Sin embargo, en la Tarea que devolvió el método StartNew, como en cualquier otro, puede llamar al método ConfigureAwait con el parámetro falso, luego la ejecución después de esperar continuará no en el contexto capturado, sino en uno arbitrario. Esto siempre debe hacerse cuando el contexto de ejecución no es importante para el código después de esperar. También es una recomendación de MS al escribir código que vendrá empaquetado en forma de biblioteca.
Detengámonos un poco más sobre cómo puede esperar hasta la finalización de la Tarea. A continuación se muestra un código de muestra, con comentarios, cuando la espera se realiza condicionalmente buena y cuando es condicionalmente mala.
public static async void AnotherMethod() { int result = await AsyncMethod();
En el primer ejemplo, esperamos que la Tarea se complete y sin bloquear el hilo de llamada, volveremos a procesar el resultado solo cuando ya esté allí, hasta que el hilo de llamada se deje solo.
En la segunda opción, bloqueamos el hilo de llamada hasta que se calcule el resultado del método. Esto es malo no solo porque tomamos el hilo, un recurso tan valioso del programa, por simple inactividad, sino también porque si el código del método que llamamos ha esperado, y el contexto de sincronización implica volver al hilo de llamada después de esperar, entonces obtendremos un punto muerto : el hilo de llamada espera hasta que se calcule el resultado del método asincrónico, el método asincrónico intenta en vano continuar su ejecución en el hilo de llamada.
Otro inconveniente de este enfoque es el complicado manejo de errores. El hecho es que los errores en el código asincrónico cuando se usa async / await son muy fáciles de manejar: se comportan como si el código fuera síncrono. Si bien, si aplicamos
exorcismo, expectativa sincrónica a la tarea, la excepción original se convierte en una excepción agregada, es decir Para manejar una excepción, tendrá que examinar el tipo InnerException y escribir la cadena if dentro de un bloque catch o usar el catch cuando se construye en lugar de la cadena de bloque catch más familiar en C #.
El tercer y último ejemplo también están marcados como malos por la misma razón y contienen los mismos problemas.
Los métodos WhenAny y WhenAll son extremadamente convenientes para esperar un grupo de Task'ov, envuelven un grupo de Task'ov en uno, que funcionará en la primera operación de Task'a del grupo o cuando todos terminen su ejecución.
Parada de flujo
Por varias razones, puede ser necesario detener la transmisión después de que comience. Hay varias formas de hacer esto. La clase Thread tiene dos métodos con nombres apropiados:
Abortar e
Interrumpir . El primero no se recomienda para su uso, ya que después de que se llame en cualquier momento aleatorio, durante el procesamiento de cualquier instrucción, se lanzará una
ThreadAbortedException . No espera que una excepción de este tipo se bloquee al incrementar una variable entera, ¿verdad? Y cuando se usa este método, esta es una situación muy real. Si desea evitar que el CLR arroje dicha excepción en una sección específica del código, puede envolverla en llamadas a
Thread.BeginCriticalRegion ,
Thread.EndCriticalRegion . Cualquier código escrito en un bloque finalmente está envuelto con tales llamadas. Por esta razón, en las entrañas del código marco, puede encontrar bloques con un intento vacío, pero no un vacío finalmente. Microsoft no recomienda usar este método que no lo incluyeron en .net core.
El método de interrupción funciona de manera más predecible. Puede interrumpir un subproceso con la excepción de
ThreadInterruptedException solo cuando el subproceso está en estado inactivo. En este estado, se suspende mientras espera WaitHandle, lock o después de llamar a Thread.Sleep.
Ambas opciones descritas anteriormente son malas para su imprevisibilidad. La solución es usar la estructura
CancellationToken y la clase
CancellationTokenSource . La conclusión es: se crea una instancia de la clase CancellationTokenSource y solo la persona propietaria puede detener la operación llamando al método
Cancel . Solo el CancellationToken se pasa a la operación misma. Los propietarios de CancellationToken no pueden cancelar la operación ellos mismos, pero solo pueden verificar si la operación ha sido cancelada. Para hacer esto, hay una propiedad booleana
IsCancellationRequested y el método
ThrowIfCancelRequested . Este último generará una
TaskCancelledException si se llama al método Cancel en la instancia CancellationToken cancelada de CancellationTokenSource. Y es este método el que recomiendo usar. Esto es mejor que las opciones anteriores al obtener el control total sobre en qué puntos se puede interrumpir la operación de excepción.
La opción más cruel para detener el hilo es llamar a la función Win32 API TerminateThread. El comportamiento del CLR después de llamar a esta función puede ser impredecible. En MSDN, se escribe lo siguiente sobre esta función:
“TerminateThread es una función peligrosa que solo debe usarse en los casos más extremos. "Convierta API heredada a basada en tareas utilizando el método FromAsync
Si tiene la suerte de trabajar en un proyecto que se inició después de que se introdujeron las tareas y dejó de causar un horror silencioso para la mayoría de los desarrolladores, entonces no tendrá que lidiar con muchas API antiguas, tanto de terceros como de las que su equipo torturó en el pasado. Afortunadamente, el equipo de desarrollo de .NET Framework nos cuidó, aunque quizás el objetivo era cuidarnos a nosotros mismos. Sea como fuere, .NET tiene una serie de herramientas para convertir sin problemas el código escrito en viejos enfoques de programación asincrónica a uno nuevo. Uno de ellos es el método FromAsync de TaskFactory. Usando el siguiente ejemplo de código, envuelvo los viejos métodos asincrónicos de la clase WebRequest en Tarea usando este método.
object state = null; WebRequest wr = WebRequest.CreateHttp("http://github.com"); await Task.Factory.FromAsync( wr.BeginGetResponse, we.EndGetResponse );
Este es solo un ejemplo y es poco probable que lo haga con los tipos integrados, pero cualquier proyecto antiguo simplemente está repleto de métodos BeginDoSomething que devuelven los métodos IAsyncResult y EndDoSomething que lo aceptan.Convierta API heredada a basada en tareas usando la clase TaskCompletionSource
Otra herramienta importante a considerar es la clase
TaskCompletionSource . En términos de funciones, propósito y principio de funcionamiento, de alguna manera puede recordar el método RegisterWaitForSingleObject de la clase ThreadPool sobre la que escribí anteriormente. Con esta clase, puede envolver API asincrónicas antiguas de manera fácil y conveniente en Task.
Dirás que ya hablé sobre el método FromAsync de la clase TaskFactory destinada a estos fines. Aquí tendremos que recordar toda la historia del desarrollo de modelos asíncronos en .net que Microsoft ha estado ofreciendo durante los últimos 15 años: antes del Patrón Asíncrono Basado en Tareas (TAP) había un Patrón de Programación Asincrónico (APP), que trataba sobre los métodos Begin DoSomething que devuelven los métodos IAsyncResult y End DoSomething que lo aceptan y el método FromAsync está bien para el legado de estos años, pero con el tiempo, fue reemplazado por un patrón asincrónico basado en eventos ( EAP ), que suponía que se llamaría a un evento al finalizar la operación asincrónica.TaskCompletionSource es excelente para envolver en Task y API heredada construida alrededor del modelo de evento. La esencia de su trabajo es la siguiente: un objeto de esta clase tiene una propiedad pública de tipo Tarea cuyo estado puede controlarse mediante los métodos SetResult, SetException, etc. de la clase TaskCompletionSource. En los lugares donde se aplicó el operador de espera a esta Tarea, se ejecutará o se bloqueará con una excepción, según el método aplicado a TaskCompletionSource. Si todo aún no está claro, veamos este ejemplo de código, donde alguna API EAP antigua está envuelta en la Tarea usando TaskCompletionSource: cuando se dispara el evento, la Tarea se transferirá al estado Completado, y el método que aplicó el operador de espera a esta Tarea reanudará la ejecución obteniendo el objeto
resultado .
public static Task<Result> DoAsync(this SomeApiInstance someApiObj) { var completionSource = new TaskCompletionSource<Result>(); someApiObj.Done += result => completionSource.SetResult(result); someApiObj.Do(); result completionSource.Task; }
TaskCompletionSource Consejos y trucos
Ajustar las API más antiguas no es todo lo que puede hacer con TaskCompletionSource. El uso de esta clase abre una posibilidad interesante de diseñar varias API en tareas que no ocupan subprocesos. Y el flujo, como recordamos, es un recurso costoso y su número es limitado (principalmente por RAM). Esta limitación se logra fácilmente mediante el desarrollo, por ejemplo, de una aplicación web cargada con lógica empresarial compleja. Considere las posibilidades de las que estoy hablando para implementar un truco como Long-Polling.
En resumen, la esencia del truco es la siguiente: necesita obtener información de la API sobre algunos eventos que ocurren de forma paralela, mientras que la API por alguna razón no puede informar el evento, pero solo puede devolver el estado. Un ejemplo de esto es todas las API creadas sobre HTTP antes de los tiempos de WebSocket o cuando es imposible por alguna razón usar esta tecnología. El cliente puede preguntarle al servidor HTTP. Un servidor HTTP no puede provocar la comunicación con un cliente. Una solución simple es interrogar al servidor por temporizador, pero esto crea una carga adicional en el servidor y un retraso adicional en promedio TimerInterval / 2. Para solucionar este problema, se inventó un truco llamado Long Polling, que consiste en retrasar la respuesta del servidor hasta que expire el tiempo de espera o Un evento sucederá. Si se ha producido un evento, se procesa; de lo contrario, la solicitud se vuelve a enviar. while(!eventOccures && !timeoutExceeded) { CheckTimout(); CheckEvent(); Thread.Sleep(1); }
Pero tal solución se mostrará terriblemente tan pronto como aumente el número de clientes que esperan el evento, porque Cada uno de estos clientes, en previsión del evento, ocupa una secuencia completa. Sí, y tenemos un retraso adicional de 1 ms en la operación del evento, la mayoría de las veces no es significativo, pero ¿por qué empeorar el software de lo que puede ser? Si elimina Thread.Sleep (1), en vano cargaremos un núcleo de procesador al 100% inactivo, girando en un ciclo inútil. Con TaskCompletionSource, puede rehacer fácilmente este código y resolver todos los problemas identificados anteriormente:
class LongPollingApi { private Dictionary<int, TaskCompletionSource<Msg>> tasks; public async Task<Msg> AcceptMessageAsync(int userId, int duration) { var cs = new TaskCompletionSource<Msg>(); tasks[userId] = cs; await Task.WhenAny(Task.Delay(duration), cs.Task); return cs.Task.IsCompleted ? cs.Task.Result : null; } public void SendMessage(int userId, Msg m) { if (tasks.TryGetValue(userId, out var completionSource)) completionSource.SetResult(m); } }
Este código no está listo para producción, sino solo una demostración. Para usarlo en casos reales, debe al menos manejar la situación cuando llega un mensaje en un momento en que nadie lo espera: en este caso, el método AsseptMessageAsync debería devolver una tarea ya completada. Si este caso es el más frecuente, puede pensar en usar ValueTask.Al recibir una solicitud de mensaje, creamos y colocamos TaskCompletionSource en el diccionario, y luego esperamos lo que sucede primero: el intervalo de tiempo especificado expira o se recibe un mensaje.
ValueTask: por qué y cómo
Los operadores asíncronos / en espera, como el operador de retorno de rendimiento, generan una máquina de estado a partir del método, que está creando un nuevo objeto, que casi siempre no es importante, pero en casos raros puede crear un problema. Este caso puede ser un método llamado con mucha frecuencia, que habla de decenas y cientos de miles de llamadas por segundo. Si dicho método se escribe de modo que en la mayoría de los casos devuelva un resultado sin pasar por todos los métodos en espera, .NET proporciona una herramienta para optimizar esto: la estructura ValueTask. Para que quede claro, considere un ejemplo de su uso: hay un caché al que vamos muy a menudo. Hay algunos valores en él y luego los devolvemos, si no, entonces vamos a algún IO lento detrás de ellos. Quiero hacer esto último de forma asíncrona, lo que significa que todo el método es asíncrono. Por lo tanto, la forma obvia de escribir un método es la siguiente:
public async Task<string> GetById(int id) { if (cache.TryGetValue(id, out string val)) return val; return await RequestById(id); }
Debido al deseo de optimizar un poco y un ligero temor a lo que generará Roslyn al compilar este código, podemos reescribir este ejemplo de la siguiente manera: public Task<string> GetById(int id) { if (cache.TryGetValue(id, out string val)) return Task.FromResult(val); return RequestById(id); }
De hecho, la solución óptima en este caso es optimizar la ruta de acceso directo, es decir, obtener el valor del diccionario sin asignaciones adicionales y cargar en el GC, mientras que en esos raros casos cuando aún tenemos que ir al IO, todo seguirá siendo positivo. / menos viejo: public ValueTask<string> GetById(int id) { if (cache.TryGetValue(id, out string val)) return new ValueTask<string>(val); return new ValueTask<string>(RequestById(id)); }
Echemos un vistazo más de cerca a este fragmento de código: si hay un valor en la memoria caché, creamos una estructura, de lo contrario, la tarea real se envolverá en una significativa. Al código de llamada no le importa de qué manera se ejecutó este código: ValueTask desde el punto de vista de la sintaxis de C # se comportará como la Tarea habitual en este caso.TaskSchedulers: gestión de estrategias de lanzamiento de tareas
La siguiente API que me gustaría considerar es la clase TaskScheduler y sus derivados. Ya mencioné anteriormente que en TPL existe la capacidad de controlar las estrategias para distribuir Task'ov por hilo. Dichas estrategias se definen en los descendientes de la clase TaskScheduler. Casi cualquier estrategia que pueda ser necesaria se encontrará en la biblioteca ParallelExtensionsExtras , desarrollada por microsoft, pero no es parte de .NET, pero se entrega como un paquete Nuget. Consideremos brevemente algunos de ellos:- CurrentThreadTaskScheduler : realiza la tarea en el hilo actual
- LimitedConcurrencyLevelTaskScheduler : limita el número de tareas ejecutadas simultáneamente al parámetro N, que se acepta en el constructor
- OrderedTaskScheduler — LimitedConcurrencyLevelTaskScheduler(1), .
- WorkStealingTaskScheduler — work-stealing . ThreadPool. , .NET ThreadPool , , . . .. WorkStealingTaskScheduler' , ThreadPool .
- QueuedTaskScheduler : le permite realizar tareas de acuerdo con las reglas de la cola con prioridades
- ThreadPerTaskScheduler : crea un hilo separado para cada tarea que se ejecuta en él. Puede ser útil para tareas que se ejecutan de forma impredecible.
Hay un buen artículo detallado sobre TaskSchedulers en el blog de Microsoft.Para una depuración conveniente de todo lo relacionado con Tareas en Visual Studio, hay una ventana de Tareas. En esta ventana, puede ver el estado actual de la tarea e ir a la línea de código que se está ejecutando actualmente.
PLinq y la clase paralela
Además de Task y todo lo que se dijo con ellos en .NET, hay dos herramientas más interesantes: PLinq (Linq2Parallel) y la clase Parallel. El primero promete la ejecución paralela de todas las operaciones de Linq en múltiples hilos. El número de subprocesos se puede configurar con el método de extensión WithDegreeOfParallelism. Desafortunadamente, la mayoría de las veces PLinq en el modo de ejecución por defecto no tendrá suficiente información sobre el interior de su fuente de datos para proporcionar una ganancia de velocidad significativa, por otro lado, el precio de intento es muy bajo: solo necesita llamar al método AsParallel frente a la cadena del método Linq y realizar pruebas de rendimiento. Además, es posible transferir a PLinq información adicional sobre la naturaleza de su fuente de datos utilizando el mecanismo de Particiones. Puedes leer más aquí y aquí..La clase estática paralela proporciona métodos para iterar sobre una colección Foreach en paralelo, ejecutar un bucle For y ejecutar múltiples delegados en paralelo a Invoke. La ejecución del hilo actual se detendrá hasta el final de los cálculos. El número de subprocesos se puede configurar pasando ParallelOptions como último argumento. Usando opciones, también puede especificar TaskScheduler y CancellationToken.Conclusiones
Cuando comencé a escribir este artículo basado en los materiales de mi informe y la información que recolecté durante el trabajo posterior, no esperaba que resultara tanto. Ahora, cuando el editor de texto en el que estoy escribiendo este artículo me dice con reproche que la página 15 ha desaparecido, resumiré los resultados intermedios. Otros trucos, API, herramientas visuales y dificultades se discutirán en un artículo futuro.Conclusiones:- Debe conocer las herramientas para trabajar con subprocesos, asincronía y paralelismo para utilizar los recursos de las PC modernas.
- .NET tiene muchas herramientas diferentes para este propósito.
- No todos aparecieron a la vez, porque a menudo se puede encontrar legado, sin embargo, hay formas de convertir API antiguas sin mucho esfuerzo.
- El trabajo con subprocesos en .NET está representado por las clases Thread y ThreadPool
- Thread.Abort, Thread.Interrupt, Win32 API TerminateThread . CancellationToken'
- — , . , . TaskCompletionSource
- .NET Task'.
- c# async/await
- Task' TaskScheduler'
- ValueTask hot-paths memory-traffic
- Tasks Threads Visual Studio
- PLinq , , partitioning
- ...