threads thread new multithread multiple multi example creating multithreading

multithreading - thread - Mejores prácticas de roscado



multithread demo (11)

Muchos de los proyectos en los que trabajo tienen implementaciones de subprocesos deficientes y yo soy el tonto que tiene que rastrearlas. ¿Hay una mejor manera aceptada para manejar el enhebrado? Mi código siempre está esperando un evento que nunca se dispara.

Estoy pensando un poco como un patrón de diseño o algo así.


Es el estado mutable, estúpido.

Esa es una cita directa de Java Concurrency in Practice de Brian Goetz. Aunque el libro está centrado en Java, el "Resumen de la Parte I" ofrece algunos otros consejos útiles que se aplicarán en muchos contextos de programación de subprocesos. Aquí hay algunos más de ese mismo resumen:

  • Los objetos inmutables son automáticamente seguros para hilos.
  • Guarda cada variable mutable con un candado.
  • Un programa que accede a una variable mutable desde múltiples subprocesos sin sincronización es un programa roto.

Recomendaría obtener una copia del libro para un tratamiento en profundidad de este tema difícil.

texto alt http://www.cs.umd.edu/class/fall2008/cmsc433/jcipMed.jpg


(Al igual que Jon Skeet, gran parte de esto supone .NET)

A riesgo de parecer argumentativo, comentarios como estos solo me molestan:

Aprender a escribir programas de múltiples subprocesos correctamente es extremadamente difícil y consume mucho tiempo.

Deben evitarse los hilos cuando sea posible ...

Es prácticamente imposible escribir software que haga algo significativo sin aprovechar los hilos en alguna capacidad. Si está en Windows, abra su Administrador de tareas, habilite la columna Recuento de subprocesos y probablemente pueda contar con una mano el número de procesos que utilizan un solo subproceso. Sí, uno no debería simplemente usar hilos por el simple hecho de usar hilos, ni debería hacerse con cautela, pero francamente, creo que estos clichés se usan con demasiada frecuencia.

Si tuviera que reducir la programación multiproceso para el verdadero principiante, diría esto:

  • Antes de saltar a él, primero entienda que el límite de la clase no es lo mismo que un límite de hilo. Por ejemplo, si un método de devolución de llamada en su clase es llamado por otro hilo (por ejemplo, el delegado de AsyncCallback al método TcpListener.BeginAcceptTcpClient ()), entienda que la devolución de llamada se ejecuta en ese otro hilo. Entonces, aunque la devolución de llamada se produce en el mismo objeto, aún tiene que sincronizar el acceso a los miembros del objeto dentro del método de devolución de llamada. Los hilos y las clases son ortogonales; Es importante entender este punto.
  • Identifique qué datos deben compartirse entre los subprocesos. Una vez que haya definido los datos compartidos, intente consolidarlos en una sola clase si es posible.
  • Limite los lugares donde se pueden escribir y leer los datos compartidos. Si puede llevar esto a un lugar para escribir y un lugar para leer, se estará haciendo un tremendo favor. Esto no siempre es posible, pero es un buen objetivo para el que hay que disparar.
  • Obviamente, asegúrese de sincronizar el acceso a los datos compartidos utilizando la clase Monitor o la palabra clave de bloqueo.
  • Si es posible, use un solo objeto para sincronizar sus datos compartidos, independientemente de cuántos campos compartidos haya. Esto simplificará las cosas. Sin embargo, también puede restringir demasiado las cosas, en cuyo caso, es posible que necesite un objeto de sincronización para cada campo compartido. Y en este punto, usar clases inmutables se vuelve muy útil.
  • Si tiene un subproceso que necesita señalar otro (s) subproceso (s), le recomendaría encarecidamente que use la clase ManualResetEvent para hacer esto en lugar de usar eventos / delegados.

En resumen, diría que el enhebrado no es difícil, pero puede ser tedioso. Sin embargo, una aplicación con subprocesos adecuados será más receptiva y sus usuarios lo apreciarán más.

EDITAR: No hay nada "extremadamente difícil" en ThreadPool.QueueUserWorkItem (), delegados asíncronos, los diversos pares de métodos BeginXXX / EndXXX, etc. en C #. En todo caso, estas técnicas hacen que sea mucho más fácil llevar a cabo varias tareas de una manera con hilos. Si tiene una aplicación GUI que hace una gran base de datos, socket o interacción de E / S, es prácticamente imposible hacer que el front-end responda al usuario sin aprovechar los hilos entre bambalinas. Las técnicas que mencioné anteriormente hacen esto posible y son muy fáciles de usar. Es importante entender las trampas, para estar seguro. Simplemente creo que hacemos programadores, especialmente los más jóvenes, un perjuicio cuando hablamos de cuán "extremadamente difícil" es la programación de subprocesos múltiples o cómo deben evitarse los hilos ". Comentarios como estos simplifican demasiado el problema y exageran el mito de que la verdad nunca ha sido tan fácil. Hay razones legítimas para usar hilos, y clichés como este me parecen contraproducentes.


(Suponiendo que .NET; cosas similares se aplicarían a otras plataformas).

Bueno, hay muchas cosas a considerar. Te aconsejo

  • La inmutabilidad es ideal para subprocesos múltiples. La programación funcional funciona bien al mismo tiempo, en parte debido al énfasis en la inmutabilidad.
  • Utilice bloqueos cuando acceda a datos compartidos mutables, tanto para lecturas como para escrituras.
  • No intente ir sin bloqueo a menos que realmente tenga que hacerlo. Las cerraduras son caras, pero rara vez el cuello de botella.
  • Monitor.Wait casi siempre debe ser parte de un bucle de condición, esperando que una condición se vuelva verdadera y espera de nuevo si no lo es.
  • Trate de evitar mantener cerraduras durante más tiempo del necesario.
  • Si alguna vez necesita adquirir dos bloqueos a la vez, documente el pedido a fondo y asegúrese de usar siempre el mismo orden.
  • Documentar el hilo de seguridad de sus tipos. La mayoría de los tipos no necesitan estar a salvo de subprocesos, simplemente no deben ser hostiles a los subprocesos (es decir, "puede usarlos desde múltiples subprocesos, pero es su responsabilidad eliminar bloqueos si desea compartirlos)
  • No acceda a la UI (excepto en formas documentadas seguras de subprocesos) desde un subproceso que no sea UI. En Windows Forms, use Control.Invoke / BeginInvoke

Eso está fuera de mi cabeza: probablemente piense en más si esto te es útil, pero me detendré allí en caso de que no lo sea.


Agregando a los puntos que otras personas ya han hecho aquí:

Algunos desarrolladores parecen pensar que el bloqueo "casi suficiente" es lo suficientemente bueno. Según mi experiencia, lo contrario puede ser cierto: el bloqueo "casi suficiente" puede ser peor que el bloqueo suficiente.

Imagina un hilo Un recurso de bloqueo R, usándolo y luego desbloqueando. A continuación, utiliza el recurso R ''sin bloqueo.

Mientras tanto, el hilo B intenta acceder a R mientras que A lo tiene bloqueado. El subproceso B se bloquea hasta que el subproceso A desbloquea R. Luego, el contexto de la CPU cambia al subproceso B, que accede a R, y luego actualiza R ''durante su segmento de tiempo . Esa actualización hace que R ''sea inconsistente con R, causando una falla cuando A intenta acceder a ella.

Pruebe en la mayor cantidad posible de arquitecturas de hardware y sistemas operativos. Diferentes tipos de CPU, diferentes números de núcleos y chips, Windows / Linux / Unix, etc.

El primer desarrollador que trabajó con programas de subprocesos múltiples fue un tipo llamado Murphy.


Aprender a escribir programas de múltiples subprocesos correctamente es extremadamente difícil y consume mucho tiempo.

Entonces, el primer paso es: reemplace la implementación con una que no use múltiples subprocesos.

Luego, con cuidado, vuelva a colocar los hilos si, y solo si, descubre una verdadera necesidad de hacerlo, cuando haya descubierto algunas formas seguras muy simples de hacerlo. Una implementación sin subprocesos que funcione de manera confiable es mucho mejor que una implementación de subprocesos rotos.

Cuando esté listo para comenzar, favorezca los diseños que utilizan colas seguras para subprocesos para transferir elementos de trabajo entre subprocesos y asegúrese de que solo se acceda a ellos desde un subproceso a la vez.

Intente evitar solo rociar bloques de lock alrededor de su código con la esperanza de que sea seguro para subprocesos. No funciona Finalmente, dos rutas de código adquirirán los mismos bloqueos en un orden diferente, y todo se detendrá (una vez cada dos semanas, en el servidor de un cliente). Esto es especialmente probable si combina subprocesos con eventos de activación, y mantiene el bloqueo mientras activa el evento; el controlador puede eliminar otro bloqueo, y ahora tiene un par de bloqueos en un orden particular. ¿Qué pasa si se sacan en el orden opuesto en alguna otra situación?

En resumen, este es un tema tan grande y difícil que creo que es potencialmente engañoso dar algunos consejos en una respuesta corta y decir "¡Ya está!" - Estoy seguro de que esa no es la intención de las muchas personas instruidas que dan respuestas aquí, pero esa es la impresión que muchos obtienen de los consejos resumidos.

En su lugar, comprar este libro .

Aquí hay un resumen muy bien redactado de este sitio :

El multihilo también tiene desventajas. Lo más importante es que puede llevar a programas mucho más complejos. Tener múltiples hilos no crea en sí mismo complejidad; Es la interacción entre los hilos lo que crea complejidad. Esto se aplica tanto si la interacción es intencional como si no, y puede resultar en largos ciclos de desarrollo, así como en una susceptibilidad continua a errores intermitentes y no reproducibles. Por esta razón, vale la pena mantener dicha interacción en un diseño de subprocesos múltiples, o no usar el subprocesamiento múltiple, ¡a menos que tenga una peculiar inclinación por volver a escribir y depurar!

Resumen perfecto de Stroustrup :

La forma tradicional de lidiar con la concurrencia dejando un montón de hilos sueltos en un solo espacio de direcciones y luego usando bloqueos para tratar de lidiar con las carreras de datos resultantes y los problemas de coordinación es probablemente la peor en términos de corrección y comprensibilidad.


Bueno, hasta ahora todos han estado centrados en Windows / .NET, por lo que voy a hacer algunos ajustes con Linux / C.

Evite los futex a toda costa (PDF), a menos que realmente necesite recuperar parte del tiempo dedicado a los bloqueos de exclusión mutua. Actualmente estoy sacándome el pelo con los futex de Linux.

Todavía no tengo el valor de ir con soluciones prácticas sin bloqueo , pero me estoy acercando rápidamente a ese punto por pura frustración. Si pudiera encontrar una implementación buena, bien documentada y portátil de lo anterior que realmente pudiera estudiar y comprender, probablemente abandonaría los hilos por completo.

Últimamente he encontrado mucho código que usa hilos que realmente no deberían, es obvio que alguien solo quiso profesar su amor eterno por los hilos de POSIX cuando una única bifurcación (sí, solo una) hubiera hecho el trabajo.

Me gustaría poder darte un código que "simplemente funciona", "todo el tiempo". Podría, pero sería tan tonto servir como demostración (servidores y otros que inician hilos para cada conexión). En aplicaciones más complejas impulsadas por eventos, todavía (después de algunos años) tengo que escribir cualquier cosa que no tenga problemas de concurrencia misteriosa que son casi imposibles de reproducir. Así que soy el primero en admitir que, en ese tipo de aplicaciones, los hilos son solo una cuerda demasiado para mí. Son tan tentadores y siempre termino ahorcándome.


Buscar un patrón de diseño cuando se trata de hilos es el mejor enfoque para comenzar. Es una lástima que mucha gente no lo intente, en lugar de intentar implementar construcciones de subprocesos múltiples más o menos complejas por su cuenta.

Probablemente estaría de acuerdo con todas las opiniones publicadas hasta ahora. Además, recomiendo usar algunos marcos existentes más amplios, que proporcionen elementos básicos en lugar de instalaciones simples como bloqueos o operaciones de espera / notificación. Para Java, sería simplemente el paquete java.util.concurrent incorporado, que le brinda clases listas para usar que puede combinar fácilmente para lograr una aplicación de multiproceso. La gran ventaja de esto es que evita escribir operaciones de bajo nivel, lo que resulta en un código difícil de leer y propenso a errores, en favor de una solución mucho más clara.

Desde mi experiencia, parece que la mayoría de los problemas de concurrencia se pueden resolver en Java usando este paquete. Pero, por supuesto, siempre debes tener cuidado con los subprocesos múltiples, de todos modos es un desafío.


En lugar de bloquear los contenedores, debe usar ReaderWriterLockSlim. Esto le proporciona una base de datos como el bloqueo: un número infinito de lectores, un escritor y la posibilidad de actualización.

En cuanto a los patrones de diseño, pub / sub está bastante bien establecido y es muy fácil de escribir en .NET (usando readerwriterlockslim). En nuestro código, tenemos un objeto MessageDispatcher que todos reciben. Te suscribes o envías un mensaje de forma completamente asíncrona. Todo lo que tiene que bloquear es las funciones registradas y cualquier recurso en el que trabajen. Hace mucho más fácil el multihilo.


GRAN énfasis en el primer punto que Jon publicó. Cuanto más inmutable sea el estado (es decir, los globales que son constantes, etc ...), más fácil será su vida (es decir, cuantos menos bloqueos tendrá que enfrentar, menos razonamiento tendrá) hacer sobre orden de intercalación, etc ...)

Además, muchas veces, si tiene objetos pequeños a los que necesita tener acceso a varios subprocesos, a veces es mejor copiarlos entre subprocesos en lugar de tener un global mutable compartido que tiene que mantener un bloqueo para leer / mutar. Es un intercambio entre la cordura y la eficiencia de la memoria.


Me gustaría seguir con el consejo de Jon Skeet con un par de consejos más:

  • Si está escribiendo un "servidor", y es probable que tenga una gran cantidad de paralelismo de inserción, no utilice el Compacto de SQL de Microsoft. Su administrador de bloqueo es estúpido. Si utiliza SQL Compact, NO utilice transacciones serializables (que es la opción predeterminada para la clase TransactionScope). Las cosas se derrumbarán rápidamente en ti. SQL Compact no admite tablas temporales, y cuando intenta simularlas dentro de transacciones serializadas, vuelve a hacer cosas estúpidas como tomar los bloqueos x en las páginas de índice de la tabla _sysobjects. También se entusiasma mucho con la promoción de bloqueos, incluso si no usa tablas temporales. Si necesita acceso en serie a varias tablas, su mejor opción es utilizar transacciones de lectura repetibles (para brindar atomicidad e integridad) y luego implementar su propio administrador de bloqueo jerárquico basado en objetos de dominio (cuentas, clientes, transacciones, etc.), en lugar de utilizando el esquema basado en la tabla de filas de la base de datos.

    Sin embargo, cuando haces esto, debes tener cuidado (como dijo John Skeet) para crear una jerarquía de bloqueo bien definida.

  • Si crea su propio administrador de bloqueo, use los campos <ThreadStatic> para almacenar información sobre los bloqueos que toma, y ​​luego agregue <ThreadStatic> dentro del administrador de bloqueo que aplican las reglas de la jerarquía de bloqueo. Esto ayudará a erradicar los problemas potenciales por adelantado.

  • En cualquier código que se ejecute en un subproceso de la interfaz de usuario, agregue !InvokeRequired en !InvokeRequired (para winforms) o Dispatcher.CheckAccess() (para WPF). De manera similar, debe agregar el aserto inverso al código que se ejecuta en subprocesos de fondo. De esa manera, las personas que observen un método sabrán, solo con mirarlo, cuáles son los requisitos de subprocesos. Las afirmaciones también ayudarán a atrapar errores.

  • Afirmar como loco, incluso en las construcciones de venta por menor. (Eso significa lanzar, pero puedes hacer que tus lanzamientos parezcan afirmaciones). Un volcado de emergencia con una excepción que dice "violaste las reglas de subprocesamiento al hacer esto", junto con los seguimientos de pila, es mucho más fácil de depurar que un informe de un cliente en el otro lado del mundo que dice "de vez en cuando la aplicación simplemente se congela en mí, o escupe gook engreído ".


Puede estar interesado en algo como la CSP , o uno de los otros álgebras teóricas para tratar la concurrencia. Hay bibliotecas de CSP para la mayoría de los idiomas, pero si el idioma no fue diseñado para ello, se requiere un poco de disciplina para usarlo correctamente. Pero en última instancia, todo tipo de concurrencia / subprocesos se reduce a algunos conceptos básicos bastante simples: evite los datos mutables compartidos y entienda exactamente cuándo y por qué cada subproceso debe bloquearse mientras espera otro subproceso. (En CSP, los datos compartidos simplemente no existen. Cada hilo (o proceso en la terminología de CSP) solo puede comunicarse con otros a través del bloqueo de canales de paso de mensajes. Como no hay datos compartidos, las condiciones de carrera desaparecen. Desde el paso de mensajes está bloqueando, resulta fácil razonar acerca de la sincronización y, literalmente, probar que no se pueden producir puntos muertos.)

Otra buena práctica, que es más fácil de actualizar en el código existente es asignar una prioridad o nivel a cada bloqueo en su sistema, y ​​asegurarse de que las siguientes reglas se sigan de manera consistente:

  • Mientras mantiene un bloqueo en el nivel N, solo puede adquirir nuevos bloqueos de niveles inferiores
  • Se deben adquirir múltiples bloqueos al mismo nivel al mismo tiempo, como una sola operación, que siempre trata de adquirir todos los bloqueos solicitados en el mismo orden global (Tenga en cuenta que cualquier orden consistente será suficiente, pero cualquier hilo que intente adquirir uno o más bloqueos en el nivel N, deben adquirirlos en el mismo orden que cualquier otro subproceso haría en cualquier otro lugar en el código.)

Seguir estas reglas significa que es simplemente imposible que se produzca un punto muerto. Entonces solo tienes que preocuparte por los datos compartidos mutables.