El libro "Código de alto rendimiento en la plataforma .NET. 2da edicion

imagen Este libro le enseñará cómo maximizar el rendimiento del código administrado, idealmente sin sacrificar ninguno de los beneficios del entorno .NET, o en el peor de los casos, sacrificando un número mínimo de ellos. Aprenderá métodos de programación racional, descubrirá qué evitar y, muy probablemente, lo más importante, cómo usar las herramientas que están disponibles gratuitamente para medir fácilmente el nivel de productividad. El material de capacitación tendrá un mínimo de agua, solo el más necesario. El libro proporciona exactamente lo que necesita saber, es relevante y conciso, no contiene demasiado. La mayoría de los capítulos comienzan con información general y antecedentes, seguidos de consejos específicos, establecidos como una receta, y terminan con una sección de medición y depuración paso a paso para una amplia variedad de escenarios.

En el camino, Ben Watson se sumerge en componentes específicos del entorno .NET, en particular, Common Language Runtime (CLR) basado en él, y veremos cómo se gestiona la memoria de su máquina, se genera el código, se organiza la ejecución de subprocesos múltiples y se hace mucho más. . Se le mostrará cómo la arquitectura .NET al mismo tiempo limita su herramienta de software y le proporciona características adicionales y cómo la elección de las rutas de programación puede afectar significativamente el rendimiento general de la aplicación. Como beneficio adicional, el autor compartirá con usted historias de la experiencia de crear sistemas .NET muy grandes, complejos y de alto rendimiento en Microsoft durante los últimos nueve años.

Extracto: elija el tamaño de grupo de subprocesos adecuado


Con el tiempo, el grupo de subprocesos se configura de forma independiente, pero al principio no tiene un historial y comenzará en el estado inicial. Si su producto de software es excepcionalmente asíncrono y utiliza un procesador central de manera significativa, puede sufrir costos de lanzamiento iniciales prohibitivamente altos, en espera de la creación y disponibilidad de aún más hilos. Ajuste de los parámetros de inicio para que desde el momento en que se inicie la aplicación tenga a mano un cierto número de subprocesos preparados:

const int MinWorkerThreads = 25; const int MinIoThreads = 25; ThreadPool.SetMinThreads(MinWorkerThreads, MinIoThreads); 

Ten cuidado aquí. Cuando se usan objetos de Tarea, su despacho se basará en el número de subprocesos disponibles para esto. Si hay demasiados, los objetos de tareas pueden sufrir una programación excesiva, lo que al menos conducirá a una disminución en la eficiencia del procesador central debido al cambio de contexto más frecuente. Si la carga de trabajo no es tan alta, el grupo de subprocesos puede cambiar al uso de un algoritmo que puede reducir el número de subprocesos, llevándolo a un número inferior al especificado.

También puede establecer su número máximo utilizando el método SetMaxThreads, pero esta técnica está sujeta a riesgos similares.

Para averiguar el número requerido de subprocesos, deje este parámetro solo y analice su aplicación en un estado estable utilizando los métodos ThreadPool.GetMaxThreads y ThreadPool.GetMinThreads o contadores de rendimiento que muestran el número de subprocesos involucrados en el proceso.

No interrumpa los flujos.


Interrumpir el trabajo de los hilos sin coordinación con el trabajo de otros hilos es un procedimiento bastante peligroso. Los flujos deben limpiarse solos, y llamarlos método Abortar no les permite cerrarse sin consecuencias negativas. Cuando se destruye un hilo, partes de la aplicación están en un estado indefinido. Sería mejor bloquear el programa, pero idealmente se necesita un reinicio limpio.

Para terminar un subproceso de forma segura, debe usar algún tipo de estado compartido, y la función del subproceso en sí debe verificar este estado para determinar cuándo debe completarse. La seguridad debe lograrse mediante la coherencia.

En general, siempre debe usar objetos de tarea: no se proporciona una API para interrumpir una tarea. Para poder terminar un subproceso de forma coherente, debe, como se indicó anteriormente, utilizar el token CancellationToken.

No cambie la prioridad del hilo


En general, cambiar la prioridad de los hilos es una tarea extremadamente infructuosa. En Windows, el despacho de subprocesos se realiza de acuerdo con su nivel de prioridad. Si los subprocesos de alta prioridad siempre están listos para ejecutarse, se ignorarán los subprocesos de baja prioridad y, muy raramente, tendrán la oportunidad de ejecutarse. Al aumentar la prioridad de un hilo, usted dice que su trabajo debe tener prioridad sobre todos los demás trabajos, incluidos otros procesos. Esto no es seguro para un sistema estable.

Es mejor reducir la prioridad del hilo si está ejecutando algo que puede esperar hasta la finalización de las tareas de prioridad normal. Una buena razón para reducir la prioridad de un subproceso puede ser descubrir un subproceso fuera de control que ejecuta un bucle infinito. Es imposible interrumpir un subproceso de forma segura, por lo que la única forma de devolver un subproceso determinado y los recursos del procesador es reiniciar el proceso. Hasta que sea posible cerrar la transmisión y hacerlo limpiamente, reducir la prioridad de la transmisión fuera de control será una forma razonable de minimizar las consecuencias. Cabe señalar que incluso los subprocesos con una prioridad más baja seguirán garantizados para ejecutarse con el tiempo: cuanto más tiempo estén privados de los inicios, mayor será la prioridad dinámica que establecerá Windows. Una excepción es la prioridad inactiva THREAD_ - PRIORITY_IDLE, en la que el sistema operativo solo programa un subproceso para que se ejecute cuando literalmente no tiene nada más que iniciar.

Puede haber razones bien justificadas para aumentar la prioridad del flujo, por ejemplo, la necesidad de responder rápidamente a situaciones raras. Pero usar tales técnicas debe ser muy cauteloso. El envío de subprocesos en Windows se realiza independientemente de los procesos a los que pertenecen, por lo que se lanzará un subproceso de alta prioridad de su proceso a expensas no solo de sus otros subprocesos, sino también de todos los subprocesos de otras aplicaciones que se ejecutan en su sistema.

Si se utiliza un grupo de subprocesos, los cambios de prioridad se descartan cada vez que un subproceso vuelve al grupo. Si continúa administrando subprocesos básicos cuando utiliza la biblioteca de tareas paralelas, debe tener en cuenta que se pueden iniciar varias tareas en el mismo subproceso antes de que se devuelva al grupo.

Hilo de sincronización y bloqueo


Tan pronto como la conversación llega a varios hilos, se hace necesario sincronizarlos. La sincronización consiste en proporcionar acceso de solo un subproceso a un estado compartido, por ejemplo, a un campo de clase. Por lo general, los subprocesos se sincronizan utilizando objetos de sincronización como Monitor, Semaphore, ManualResetEvent, etc. Algunas veces se denominan informalmente bloqueos, y el proceso de sincronización en un subproceso específico se denomina bloqueo.

Una de las verdades fundamentales sobre las cerraduras es esta: nunca aumentan el rendimiento. En el mejor de los casos, con una primitiva de sincronización bien implementada y sin competencia, el bloqueo puede ser neutral. Conduce a detener la ejecución de trabajo útil por otros subprocesos y al hecho de que se pierde el tiempo de CPU, aumenta el tiempo de cambio de contexto y causa otras consecuencias negativas. Tienes que soportar esto porque la corrección es mucho más importante que el simple rendimiento. ¡Si el resultado incorrecto se calcula rápidamente no importa!

Antes de comenzar a resolver el problema del uso del aparato de bloqueo, consideraremos los principios más fundamentales.

¿Debo preocuparme por el rendimiento?


Justifique la necesidad de aumentar la productividad primero. Esto nos devuelve a los principios discutidos en el capítulo 1. El rendimiento no es igualmente importante para todo el código de su aplicación. No todo el código debe someterse a una optimización de enésimo grado. Como regla general, todo comienza con el "bucle interno", el código que se ejecuta con mayor frecuencia o el más crítico para el rendimiento, y se extiende en todas las direcciones hasta que los costos superen los beneficios recibidos. Hay muchas áreas en el código que son mucho menos importantes en términos de rendimiento. En tal situación, si necesita un candado, aplíquelo con calma.

Y ahora debes tener cuidado. Si su código no crítico se ejecuta en un subproceso de un grupo de subprocesos y lo bloquea durante mucho tiempo, el grupo de subprocesos puede comenzar a insertar más subprocesos para manejar otras solicitudes. Si uno o dos hilos hacen esto de vez en cuando, está bien. Pero si muchos hilos hacen tales cosas, puede surgir un problema, debido a esto, los recursos que deben hacer el trabajo real se gastan inútilmente. La inadvertencia al iniciar un programa con una carga constante significativa puede causar un impacto negativo en el sistema incluso en aquellas partes para las cuales el alto rendimiento no es importante, debido a un cambio de contexto innecesario o una participación irrazonable del grupo de subprocesos. Como en todos los demás casos, se deben tomar medidas para evaluar la situación.

¿Realmente necesitas un candado?


El mecanismo de bloqueo más efectivo es uno que no lo es. Si puede eliminar por completo la necesidad de sincronización de subprocesos, esta será la mejor manera de obtener un alto rendimiento. Este es un ideal que no es tan fácil de lograr. Por lo general, esto significa que debe asegurarse de que no haya un estado compartido mutable: cada solicitud que pasa a través de su aplicación puede procesarse independientemente de otra solicitud o de algunos datos mutables centralizados (lectura-escritura). Esta característica será el mejor escenario para lograr un alto rendimiento.

Y aún así ten cuidado. Con la reestructuración, es fácil ir por la borda y convertir el código en un desastre desordenado que nadie, incluido usted, puede resolver. No debe ir demasiado lejos a menos que la alta productividad sea realmente un factor crítico y no se pueda lograr de otra manera. Convierta el código en asíncrono e independiente, pero para que quede claro.

Si varios hilos solo leen de una variable (y no hay indicios de escritura desde un flujo), no es necesaria la sincronización. Todos los hilos pueden tener acceso ilimitado. Esto se aplica automáticamente a objetos inmutables, como cadenas o valores de tipos inmutables, pero se puede aplicar a cualquier tipo de objetos si garantiza la inmutabilidad de su valor durante la lectura por múltiples hilos.

Si hay varios subprocesos que escriben en alguna variable compartida, vea si se puede eliminar el acceso sincronizado al usar una variable local. Si puede crear una copia temporal para el trabajo, la necesidad de sincronización desaparecerá. Esto es especialmente importante para el acceso sincronizado repetido. Para volver a acceder a la variable compartida, debe pasar a volver a acceder a la variable local después del acceso único a la variable compartida, como en el siguiente ejemplo simple de agregar elementos a una colección compartida por varios subprocesos.

 object syncObj = new object(); var masterList = new List<long >(); const int NumTasks = 8; Task[] tasks = new Task[NumTasks]; for (int i = 0; i < NumTasks; i++) { tasks[i] = Task.Run(()=> { for (int j = 0; j < 5000000; j++) { lock (syncObj) { masterList.Add(j); } } }); } Task.WaitAll(tasks); 

Este código se puede convertir de la siguiente manera:

 object syncObj = new object(); var masterList = new List<long >(); const int NumTasks = 8; Task[] tasks = new Task[NumTasks]; for (int i = 0; i < NumTasks; i++) { tasks[i] = Task.Run(()=> { var localList = new List<long >(); for (int j = 0; j < 5000000; j++) { localList.Add(j); } lock (syncObj) { masterList.AddRange(localList); } }); } Task.WaitAll(tasks); 

En mi máquina, la segunda versión del código se ejecuta más del doble de rápido que la primera.
En última instancia, un estado compartido mutable es un enemigo fundamental del rendimiento. Requiere sincronización para la seguridad de los datos, lo que degrada el rendimiento. Si su diseño tiene al menos la más mínima oportunidad de evitar el bloqueo, entonces está cerca de implementar un sistema multihilo ideal.

Orden de preferencia de sincronización


Al decidir si es necesario algún tipo de sincronización, debe entenderse que no todos tienen el mismo rendimiento o características de comportamiento. En la mayoría de las situaciones, solo necesita usar un candado, y generalmente esta debería ser la opción original. El uso de algo diferente al bloqueo, para justificar una complejidad adicional, requiere mediciones intensivas. En general, consideramos los mecanismos de sincronización en el siguiente orden.

1. Monitor de bloqueo / clase: mantiene la simplicidad, la comprensión del código y proporciona un buen equilibrio de rendimiento.

2. La completa falta de sincronización. Deshágase de los estados mutables compartidos, reestructura y optimice. Esto es más difícil, pero si tiene éxito, básicamente funcionará mejor que aplicar bloqueo (excepto cuando se cometen errores o se degrada la arquitectura).

3. Métodos simples de enclavamiento entrelazados: en algunos escenarios puede ser más adecuado, pero tan pronto como la situación se vuelva más complicada, proceda a usar el candado.

Y, por último, si realmente puede probar los beneficios de su uso, use bloqueos más complejos y complejos (tenga en cuenta: rara vez resultan ser tan útiles como espera):

  1. bloqueos asincrónicos (se discutirán más adelante en este capítulo);
  2. todos los demás

Circunstancias específicas pueden dictar o impedir el uso de algunas de estas tecnologías. Por ejemplo, es improbable que la combinación de varios métodos entrelazados supere a una sola declaración de bloqueo.

»Se puede encontrar más información sobre el libro en el sitio web del editor
» Contenidos
» Extracto

Cupón de 25% de descuento para vendedores ambulantes - .NET

Tras el pago de la versión en papel del libro, se envía un libro electrónico por correo electrónico.

Source: https://habr.com/ru/post/458520/


All Articles