Programas multiproceso. Arquitecturas de aplicaciones multiproceso. Procesamiento paralelo en VB6

mi Este artículo no es para domadores de pitones experimentados, para quienes desenredar esta maraña de serpientes es un juego de niños, sino más bien una descripción general superficial de las posibilidades de subprocesos múltiples para los nuevos adictos a los pitones.

Desafortunadamente, no hay tanto material en ruso sobre el tema de subprocesos múltiples en Python, y los pythoners que no han oído nada, por ejemplo, sobre GIL, comenzaron a acercarse a mí con envidiable regularidad. En este artículo, intentaré describir las características más básicas de una pitón multiproceso, decirle qué es GIL y cómo vivir con ella (o sin ella), y mucho más.


Python es un lenguaje de programación encantador. Combina perfectamente muchos paradigmas de programación. La mayoría de las tareas que un programador puede encontrar se resuelven aquí de manera fácil, elegante y concisa. Pero para todas estas tareas, una solución de subproceso único suele ser suficiente, y los programas de subproceso único suelen ser predecibles y fáciles de depurar. Lo que no se puede decir sobre los programas de subprocesos múltiples y procesos múltiples.

Aplicaciones de subprocesos múltiples


Python tiene un módulo enhebrar , y tiene todo lo que necesita para la programación de subprocesos múltiples: también tiene diferente tipo cerraduras, y un semáforo, y un mecanismo de eventos. Una palabra: todo lo que se necesita para la gran mayoría de los programas de subprocesos múltiples. Además, utilizar todas estas herramientas es bastante sencillo. Considere un programa de ejemplo que inicia 2 subprocesos. Un hilo escribe diez "0", el otro - diez "1", y estrictamente en orden.

importar hilos

definitivamente escritor

para i en xrange (10):

imprimir x

Evento_para_conjunto.set()

# eventos de inicio

e1 = subprocesamiento.Evento()

e2 = enhebrar.Evento()

# subprocesos de inicio

0 , e1, e2))

1 , e2, e1))

# iniciar hilos

t1.inicio()

t2.inicio()

t1.unirse()

t2.unirse()


Sin magia, sin código vudú. El código es claro y consistente. Además, como puede ver, creamos un hilo a partir de una función. Para tareas pequeñas, esto es muy conveniente. Este código también es bastante flexible. Supongamos que tenemos un tercer proceso que escribe "2", entonces el código se verá así:

importar hilos

definitivamente escritor (x, evento_para_esperar, evento_para_establecer):

para i en xrange (10):

Event_for_wait.wait() # espera por el evento

Event_for_wait.clear() # evento limpio para el futuro

imprimir x

Evento_para_conjunto.set() # establecer evento para subproceso vecino

# eventos de inicio

e1 = subprocesamiento.Evento()

e2 = enhebrar.Evento()

e3 = enhebrar.Evento()

# subprocesos de inicio

t1 = subprocesamiento.Subproceso(objetivo=escritor, argumentos=( 0 , e1, e2))

t2 = subprocesamiento.Subproceso(objetivo=escritor, argumentos=( 1 , e2, e3))

t3 = subprocesamiento.Subproceso(objetivo=escritor, argumentos=( 2 , e3, e1))

# iniciar hilos

t1.inicio()

t2.inicio()

t3.inicio()

e1.set() # iniciar el primer evento

# unir hilos al hilo principal

t1.unirse()

t2.unirse()

t3.unirse()


Hemos agregado un nuevo evento, un nuevo hilo y hemos cambiado ligeramente los parámetros con los que
comienzan los subprocesos (por supuesto, puede escribir una solución más general usando, por ejemplo, MapReduce, pero esto ya está más allá del alcance de este artículo).
Como puedes ver, todavía no hay magia. Todo es simple y claro. Vayamos más lejos.

Bloqueo de intérprete global


Hay dos razones más comunes para usar subprocesos: primero, para aumentar la eficiencia del uso de la arquitectura multinúcleo de los procesadores modernos y, por lo tanto, el rendimiento del programa;
en segundo lugar, si necesitamos dividir la lógica del programa en secciones paralelas total o parcialmente asíncronas (por ejemplo, para poder hacer ping a varios servidores al mismo tiempo).

En el primer caso, nos encontramos ante una limitación de Python (o más bien de su implementación principal, CPython), como el Global Interpreter Lock (o GIL para abreviar). El concepto de GIL es que solo se puede ejecutar un subproceso en un procesador a la vez. Esto se hace para que no haya lucha entre subprocesos por variables individuales. El hilo ejecutable tiene acceso a todo el entorno. Esta característica de la implementación de subprocesos en Python simplifica enormemente el trabajo con subprocesos y proporciona cierta seguridad de subprocesos (thread safety).

Pero hay un punto sutil aquí: puede parecer que una aplicación de subprocesos múltiples se ejecutará exactamente la misma cantidad de tiempo que una de un solo subproceso haciendo lo mismo, o por la suma del tiempo de ejecución de cada subproceso en la CPU. . Pero aquí estamos esperando un efecto desagradable. Considere el programa:

con open("test1.txt" , "w" ) como resultado:

para i en xrange (1000000):

imprimir >> fuera, 1


Este programa simplemente escribe un millón de líneas "1" en un archivo y lo hace en ~0,35 segundos en mi computadora.

Considere otro programa:

de subprocesos de importación Subproceso

def escritor(nombre de archivo, n):

con open(filename, "w") como fuera:

para i en xrange(n):

imprimir >> fuera, 1

t1 = Subproceso (objetivo = escritor, argumentos = ("prueba2.txt" , 500000 ,))

t2 = Subproceso (objetivo = escritor, argumentos = ("prueba3.txt" , 500000 ,))

t1.inicio()

t2.inicio()

t1.unirse()

t2.unirse()


Este programa crea 2 hilos. En cada transmisión, escribe medio millón de líneas "1" en un archivo separado. De hecho, la cantidad de trabajo es la misma que la del programa anterior. Pero con el tiempo, aquí se obtiene un efecto interesante. El programa puede ejecutarse desde 0,7 segundos hasta 7 segundos. ¿Por qué está pasando esto?

Esto se debe a que cuando un hilo no necesita el recurso de la CPU, libera el GIL, y en ese momento puede intentar conseguirlo, y otro hilo, y también el hilo principal. Donde sistema operativo, sabiendo que hay muchos núcleos, puede exacerbar todo al intentar distribuir hilos entre los núcleos.

UPD: en este momento en Python 3.2, hay una implementación mejorada de la GIL, que soluciona parcialmente este problema, en particular, debido al hecho de que cada hilo, después de perder el control, espera un breve período de tiempo antes de poder capturar de nuevo la GIL (hay es una buena presentación sobre este tema en inglés)

"¿Entonces no puedes escribir programas eficientes de subprocesos múltiples en Python?", Preguntas. No, por supuesto, hay una salida, e incluso varias.

Aplicaciones Multiproceso


Para solucionar un poco el problema descrito en el párrafo anterior, Python tiene un módulo subproceso . Podemos escribir un programa que queremos ejecutar en un hilo paralelo (en realidad ya es un proceso). Y ejecútelo en uno o más subprocesos en otro programa. De esta manera, realmente aceleraría nuestro programa, porque los subprocesos creados en el iniciador GIL no se activan, sino que solo esperan a que finalice el proceso en ejecución. Sin embargo, hay muchos problemas con este método. El principal problema es que se vuelve difícil transferir datos entre procesos. Sería necesario serializar objetos de alguna manera, establecer comunicación a través de PIPE u otras herramientas, pero todo esto inevitablemente genera una sobrecarga y el código se vuelve difícil de entender.

Aquí podemos usar un enfoque diferente. Python tiene un módulo de multiprocesamiento . Funcionalmente, este módulo se parece a enhebrar . Por ejemplo, los procesos se pueden crear de la misma manera a partir de funciones ordinarias. Los métodos para trabajar con procesos son casi los mismos que para los subprocesos del módulo de subprocesos. Pero para sincronizar procesos e intercambiar datos, se acostumbra utilizar otras herramientas. Se trata de sobre colas (Queue) y canales (Pipe). Sin embargo, también hay análogos de bloqueos, eventos y semáforos que estaban en subprocesos.

Además, el módulo de multiprocesamiento tiene un mecanismo para trabajar con memoria compartida. Para ello, el módulo dispone de clases variables (Value) y array (Array) que se pueden “generalizar” (compartir) entre procesos. Para la comodidad de trabajar con variables compartidas, puede utilizar las clases de administrador (Administrador). Son más flexibles y fáciles de manejar, pero más lentos. Sin mencionar la buena capacidad de compartir tipos desde el módulo ctypes usando el módulo multiprocessing.sharedctypes.

El módulo de multiprocesamiento también tiene un mecanismo para crear grupos de procesos. Este mecanismo es muy útil para implementar el patrón Maestro-Trabajador, o para implementar un Mapa paralelo (que es, en cierto sentido, un caso especial del Maestro-Trabajador).

De los principales problemas de trabajar con el módulo de multiprocesamiento, vale la pena señalar la relativa dependencia de la plataforma de este módulo. Dado que el trabajo con procesos se organiza de manera diferente en diferentes sistemas operativos, se imponen algunas restricciones en el código. Por ejemplo, en Windows no existe un mecanismo de bifurcación, por lo que el punto de separación del proceso debe incluirse en:

si __nombre__ == "__principal__" :


Sin embargo, este diseño ya es una buena forma.

Qué más...


Existen otras bibliotecas y enfoques para escribir aplicaciones paralelas en Python. Por ejemplo, puede usar Hadoop+Python o varias implementaciones de MPI en Python (pyMPI, mpi4py). Incluso puede usar contenedores para bibliotecas C++ o Fortran existentes. Aquí fue posible mencionar marcos / bibliotecas como Pyro, Twisted, Tornado y muchos otros. Pero todo esto está más allá del alcance de este artículo.

Si te gustó mi estilo, en el próximo artículo intentaré decirte cómo escribir intérpretes simples en PLY y para qué se pueden utilizar.

La programación de subprocesos múltiples no es fundamentalmente diferente de escribir interfaces gráficas de usuario basadas en eventos e incluso de crear aplicaciones secuenciales simples. Aquí se aplican todas las reglas importantes sobre encapsulación, separación de intereses, acoplamiento flexible, etc. Pero a muchos desarrolladores les resulta difícil escribir programas multiproceso precisamente porque ignoran estas reglas. En cambio, están tratando de poner en práctica el conocimiento mucho menos importante sobre subprocesos y primitivas de sincronización extraídos de textos sobre programación multiproceso para principiantes.

Entonces, ¿cuáles son estas reglas?

Otro programador, ante un problema, piensa: "Ah, claro, necesitamos usar expresiones regulares". Y ahora tiene dos problemas: Jamie Zawinski.

Otro programador, ante un problema, piensa: "Ah, claro, usaré streams aquí". Y ahora tiene diez problemas: Bill Schindler.

Demasiados programadores que se dedican a escribir código de subprocesos múltiples se meten en problemas, como el héroe de la balada de Goethe " El aprendiz de brujo". El programador aprenderá a crear un montón de subprocesos que en principio funcionan, pero tarde o temprano se salen de control y el programador no sabe qué hacer.

Pero a diferencia del mago medio educado, el desafortunado programador no puede esperar la llegada de un poderoso hechicero que agitará su varita mágica y restaurará el orden. En cambio, el programador recurre a los trucos más feos, tratando de hacer frente a los problemas que surgen constantemente. El resultado es siempre el mismo: una aplicación excesivamente complicada, limitada, frágil y poco fiable. Tiene una amenaza persistente de interbloqueo y otros peligros inherentes al código de subprocesos múltiples incorrecto. No estoy hablando de bloqueos inexplicables, bajo rendimiento, resultados incompletos o incorrectos.

Te estarás preguntando: ¿por qué sucede esto? Existe una idea errónea común: "La programación de subprocesos múltiples es muy difícil". Pero no lo es. Si un programa de subprocesos múltiples no es confiable, generalmente falla por las mismas razones que los programas de un solo subproceso de baja calidad. Es solo que el programador no sigue los métodos de desarrollo fundamentales, conocidos y probados. Los programas de subprocesos múltiples simplemente parecen más complicados, porque cuantos más subprocesos paralelos salen mal, más desorden crean, y mucho más rápido de lo que lo haría un solo subproceso.

La idea errónea sobre la "dificultad de la programación multihilo" se ha generalizado debido a que los desarrolladores que desarrollaron profesionalmente la escritura de código de un solo hilo, se encontraron por primera vez con el multihilo y no lo manejaron. Pero en lugar de reconsiderar sus prejuicios y métodos habituales de trabajo, fijan obstinadamente lo que no quiere funcionar de ninguna manera. Justificándose por el software poco confiable y los plazos incumplidos, estas personas repiten lo mismo: "la programación de subprocesos múltiples es muy difícil".

Tenga en cuenta que arriba estoy hablando de programas típicos que usan subprocesos múltiples. De hecho, existen escenarios complejos de subprocesos múltiples, así como escenarios complejos de un solo subproceso. Pero son raros. Como regla, en la práctica, no se requiere nada sobrenatural de un programador. Movemos los datos, los transformamos, realizamos algunos cálculos de vez en cuando y finalmente almacenamos la información en una base de datos o la mostramos en la pantalla.

No hay nada difícil en mejorar el programa promedio de un solo subproceso y convertirlo en uno de subprocesos múltiples. Al menos no debería serlo. Las dificultades surgen por dos razones:

  • los programadores no saben cómo aplicar métodos de desarrollo simples, bien conocidos y probados;
  • la mayor parte de la información presentada en los libros sobre programación multiproceso es técnicamente correcta, pero es completamente inaplicable para resolver problemas aplicados.

Los conceptos de programación más importantes son universales. Se aplican por igual a programas de subproceso único y multiproceso. Los programadores que se ahogan en un torbellino de subprocesos simplemente no aprendieron lecciones importantes cuando dominaron por primera vez el código de un solo subproceso. Puedo decir esto porque tales desarrolladores cometen los mismos errores fundamentales en los programas de subprocesos múltiples y de un solo subproceso.

Quizás la lección más importante que se debe aprender en los sesenta años de historia de la programación se expresa así: estado mutable global- mal. Verdadera maldad. Los programas que dependen del estado mutable global son relativamente difíciles de razonar y, en general, no son confiables porque hay demasiadas formas de cambiar el estado. Ha habido un montón de investigaciones que respaldan este principio general, y existen innumerables patrones de diseño cuyo objetivo principal es implementar una forma u otra de ocultar datos. Para que sus programas sean más predecibles, intente eliminar el estado mutable tanto como sea posible.

En un programa secuencial de un solo subproceso, la probabilidad de corrupción de datos es directamente proporcional a la cantidad de componentes que pueden cambiar esos datos.

Como regla general, no es posible deshacerse por completo del estado global, pero el desarrollador tiene herramientas muy efectivas en el arsenal que le permiten controlar estrictamente qué componentes del programa pueden cambiar el estado. Además, aprendimos a crear capas de API restrictivas en torno a estructuras de datos primitivas. Por lo tanto, tenemos un buen control sobre cómo cambian estas estructuras de datos.

Los problemas del estado mutable global se hicieron evidentes gradualmente a finales de los 80 y principios de los 90, con el auge de la programación basada en eventos. Los programas ya no comenzaban "desde el principio" y seguían el único camino predecible de ejecución "hasta el final". Los programas modernos tienen el estado inicial, después de dejar qué eventos ocurren en ellos, en un orden impredecible, con intervalos de tiempo variables. El código sigue siendo de un solo subproceso, pero ya se vuelve asíncrono. La probabilidad de corrupción de datos aumenta precisamente porque el orden de ocurrencia de los eventos es muy importante. Hay situaciones como esta todo el tiempo: si el evento B ocurre después del evento A, entonces todo funciona bien. Pero si el evento A ocurre después del evento B, y el evento C tiene tiempo de interponerse entre ellos, entonces los datos pueden distorsionarse más allá del reconocimiento.

Si se trata de subprocesos paralelos, el problema se agrava aún más, ya que varios métodos pueden operar en el estado global al mismo tiempo. Se vuelve imposible juzgar exactamente cómo cambia el estado global. Ya no solo se pueden producir eventos en un orden impredecible, sino que también se puede actualizar el estado de varios hilos de ejecución. simultaneamente. Con la programación asíncrona, como mínimo, puede garantizar que un determinado evento no puede ocurrir hasta que otro evento haya terminado de procesarse. Es decir, es posible decir con certeza cuál será el estado global al final del procesamiento de un evento en particular. En el código de subprocesos múltiples, generalmente es imposible saber qué eventos ocurrirán en paralelo, por lo que es imposible describir el estado global en un momento dado con certeza.

Un programa de subprocesos múltiples con un extenso estado mutable global es uno de los ejemplos más reveladores del Principio de Incertidumbre de Heisenberg que conozco. Es imposible comprobar el estado de un programa sin cambiar su comportamiento.

Cuando empiezo otra filipina de estado mutable global (la esencia de esto en los párrafos anteriores), los programadores ponen los ojos en blanco y me aseguran que saben todo esto desde hace mucho tiempo. Pero si sabes esto, ¿por qué no lo dices desde tu código? Los programas están repletos de estado mutable global y los programadores se preguntan por qué el código no funciona.

No es sorprendente que el trabajo más importante en la programación de subprocesos múltiples ocurra durante la fase de diseño. Se requiere definir claramente lo que debe hacer el programa, desarrollar módulos independientes para realizar todas las funciones, describir en detalle qué datos requiere qué módulo y determinar las formas en que se intercambia información entre módulos ( Sí, no olvides preparar hermosas camisetas para todos los participantes del proyecto. Lo primero.- aprox. edición en el original). Este proceso no es fundamentalmente diferente del diseño de un programa de un solo subproceso. La clave del éxito, al igual que con el código de subproceso único, es limitar las interacciones entre módulos. Si podemos deshacernos del estado mutable compartido, entonces los problemas de intercambio de datos simplemente no surgirán.

Se podría argumentar que a veces no hay tiempo para un diseño de programa tan detallado que evite el estado global. Creo que a esto se puede y se debe dedicar tiempo. Nada perjudica más a los programas multiproceso que tratar de lidiar con el estado mutable global. Cuantos más detalles tenga que administrar, más probable es que su programa caiga en picada y se bloquee.

en realista programas de aplicación debe haber algún estado compartido que pueda cambiar. Y aquí es donde la mayoría de los programadores tienen problemas. El programador ve que aquí se requiere un estado compartido, recurre al arsenal de subprocesos múltiples y toma la herramienta más simple de allí: un bloqueo universal (sección crítica, mutex, o como lo llamen). Parecen pensar que la exclusión mutua resolverá todos los problemas de intercambio de datos.

La cantidad de problemas que pueden surgir con una sola cerradura de este tipo es simplemente asombrosa. Las condiciones de carrera, los problemas de entrada con bloqueo excesivo y los problemas de equidad son solo algunos ejemplos. Si tiene varios bloqueos, especialmente si están anidados, también deberá ocuparse de los puntos muertos, los puntos muertos dinámicos, las colas de bloqueo y otras amenazas relacionadas con la concurrencia. Además, existen problemas característicos del bloqueo único.
Cuando escribo o reviso el código, sigo una regla casi inquebrantable: si hiciste un bloqueo, entonces, aparentemente, cometiste un error en alguna parte.

Esta declaración se puede comentar de dos maneras:

  1. Si necesita un bloqueo, probablemente tenga un estado mutable global que deba protegerse de las actualizaciones simultáneas. Tener un estado mutable global es una falla que se hizo en la etapa de diseño de la aplicación. Revisión y rediseño.
  2. El bloqueo no es fácil de usar correctamente y los errores relacionados con el bloqueo pueden ser increíblemente difíciles de localizar. Es muy probable que uses el candado de forma incorrecta. Si veo un bloqueo y el programa se comporta de manera inusual, lo primero que hago es verificar el código que depende del bloqueo. Y normalmente encuentro problemas con eso.

Ambas interpretaciones son correctas.

Es fácil escribir código multiproceso. Pero es muy, muy complicado usar correctamente las primitivas de sincronización. Es posible que no esté calificado para uso correcto incluso una cuadra. Después de todo, las cerraduras y otras primitivas de sincronización son construcciones erigidas al nivel de todo el sistema. Las personas que entienden la programación paralela mucho mejor que usted usan estas primitivas para construir estructuras de datos concurrentes y construcciones de sincronización de alto nivel. Y nosotros, los programadores ordinarios, simplemente tomamos tales construcciones y las usamos en nuestro código. Programador, escritor de aplicaciones, no debe usar primitivos de sincronización de bajo nivel más de lo que hace llamadas directas a los controladores de dispositivos. Es decir, casi nunca.

Intentar usar bloqueos para resolver problemas de intercambio de datos es como apagar un fuego con oxígeno líquido. Al igual que un incendio, estos problemas son más fáciles de prevenir que de solucionar. Si se deshace del estado compartido, tampoco tiene que abusar de las primitivas de sincronización.

La mayor parte de lo que sabe sobre subprocesos múltiples es irrelevante

En los tutoriales de subprocesos múltiples para principiantes, aprenderá qué son los subprocesos. Entonces el autor considerará varias maneras, que se puede usar para configurar el funcionamiento paralelo de estos subprocesos; por ejemplo, hablará sobre el control de acceso a datos compartidos mediante bloqueos y semáforos, se detendrá en lo que puede suceder cuando se trabaja con eventos. Considere las variables de condición, las barreras de memoria, las secciones críticas, los mutexes, los campos volátiles y las operaciones atómicas en detalle. Veremos ejemplos de cómo usar estas construcciones de bajo nivel para realizar todo tipo de operaciones del sistema. Habiendo leído este material hasta la mitad, el programador decide que ya sabe lo suficiente sobre todas estas primitivas y sobre su aplicación. Después de todo, si sé cómo funciona esto a nivel de sistema, entonces puedo aplicarlo a nivel de aplicación de la misma manera. ¿Sí?

Imagina que le dices a un adolescente cómo construir tú mismo un motor de combustión interna. Luego, sin ninguna instrucción de manejo, lo pones al volante de un automóvil y dices: "¡Conduce!". El adolescente entiende cómo funciona el automóvil, pero no tiene idea de cómo llevarlo del punto A al punto B.

Comprender cómo funcionan los subprocesos a nivel del sistema generalmente no ayuda de ninguna manera a usarlos a nivel de la aplicación. No estoy sugiriendo que los programadores no necesiten aprender todos estos detalles de bajo nivel. Simplemente no espere poder aplicar este conocimiento desde el principio al diseñar o desarrollar una aplicación comercial.

La literatura introductoria de subprocesamiento (y cursos académicos relacionados) no debería enseñar construcciones de tan bajo nivel. Necesitamos centrarnos en resolver las clases de problemas más comunes y mostrar a los desarrolladores cómo se resuelven dichos problemas utilizando características de alto nivel. En principio, la mayoría de las aplicaciones comerciales son exclusivamente programas simples. Leen datos de uno o más dispositivos de entrada, realizan un procesamiento complejo en esos datos (por ejemplo, solicitan más datos en el proceso) y luego muestran los resultados.

A menudo, estos programas encajan perfectamente en el modelo proveedor-consumidor, que requiere el uso de solo tres flujos:

  • el flujo de entrada lee los datos y los coloca en la cola de entrada;
  • el subproceso de trabajo lee las entradas de la cola de entrada, las procesa y coloca los resultados en la cola de salida;
  • El subproceso de salida lee las entradas de la cola de salida y las guarda.

Estos tres subprocesos funcionan de forma independiente, la comunicación entre ellos se realiza a nivel de cola.

Aunque técnicamente estas colas pueden considerarse zonas de estado compartido, en la práctica son solo canales de comunicación que tienen su propia sincronización interna. Las colas admiten el trabajo con muchos productores y consumidores a la vez, puede agregarles y quitarles elementos en paralelo.

Dado que los pasos de entrada, procesamiento y salida están aislados entre sí, es fácil cambiar su implementación sin afectar el resto del programa. Siempre que el tipo de datos en la cola no cambie, puede refactorizar los componentes individuales del programa como desee. Además, dado que en la cola participa un número arbitrario de productores y consumidores, no será difícil agregar otros productores/consumidores. Podemos tener docenas de subprocesos de entrada que escriben información en la misma cola, o docenas de subprocesos de trabajo que toman información de la cola de entrada y digieren los datos. Dentro de una sola computadora, dicho modelo escala bien.

Pero lo más importante es que los lenguajes de programación y las bibliotecas modernas facilitan mucho la creación de aplicaciones en el modelo productor-consumidor. En .NET, encontrará Parallel Collections y la biblioteca TPL Dataflow. Java tiene un servicio Executor, así como BlockingQueue y otras clases del espacio de nombres java.util.concurrent. C++ tiene la biblioteca Boost para trabajar con subprocesos y la biblioteca Thread Building Blocks de Intel. V estudio visual 2013 de Microsoft aparecieron agentes asincrónicos. Bibliotecas similares también están disponibles en Python, JavaScript, Ruby, PHP y, que yo sepa, en muchos otros lenguajes. Puede crear una aplicación productor-consumidor con cualquiera de estos paquetes sin tener que recurrir a bloqueos, semáforos, variables de condición o cualquier otra primitiva de sincronización.

Estas bibliotecas utilizan libremente una amplia variedad de primitivas de sincronización. Esto esta bien. Todas las bibliotecas enumeradas están escritas por personas que entienden los subprocesos múltiples incomparablemente mejor que el programador promedio. Trabajar con una biblioteca de este tipo es casi lo mismo que usar una biblioteca de idiomas en tiempo de ejecución. Esto se puede comparar con la programación en un lenguaje de alto nivel en lugar de un lenguaje ensamblador.

El modelo proveedor-consumidor es solo uno de muchos ejemplos. Las bibliotecas anteriores contienen clases que se pueden usar para implementar muchos patrones de diseño de subprocesos comunes sin entrar en detalles de bajo nivel. Puede crear aplicaciones de subprocesos múltiples a gran escala sin preocuparse demasiado por cómo se coordinan y sincronizan exactamente los subprocesos.

Trabajar con bibliotecas

Por lo tanto, la creación de programas de subprocesos múltiples no es fundamentalmente diferente de la escritura de programas síncronos de un solo subproceso. Los principios importantes de encapsulación y ocultación de datos son universales, y su importancia solo aumenta cuando se involucran muchos subprocesos simultáneos. Si descuida estos aspectos importantes, incluso el conocimiento más exhaustivo del manejo de flujos de bajo nivel no lo salvará.

Los desarrolladores modernos tienen que resolver muchos problemas a nivel de programación de aplicaciones, sucede que simplemente no hay tiempo para pensar en lo que sucede a nivel de sistema. Cuanto más complejas se vuelven las aplicaciones, más detalles complejos deben ocultarse entre las capas de la API. Hemos estado haciendo esto durante más de una década. Se puede argumentar que la cualidad de ocultar la complejidad del sistema al programador es la razón principal por la que el programador logra escribir aplicaciones modernas. De hecho, ¿no ocultamos la complejidad del sistema implementando un bucle de mensajes de interfaz de usuario, construyendo protocolos de comunicación de bajo nivel, etc.?

Una situación similar surge con los subprocesos múltiples. La mayoría de los escenarios de subprocesos múltiples que el programador de aplicaciones comerciales promedio podría encontrar ya son bien conocidos y están bien implementados en las bibliotecas. Las funciones de biblioteca hacen un gran trabajo al ocultar la asombrosa complejidad de la concurrencia. Estas bibliotecas deben aprenderse a usar de la misma manera que usa las bibliotecas de elementos de la interfaz de usuario, los protocolos de comunicación y muchas otras herramientas que simplemente funcionan. Deje los subprocesos múltiples de bajo nivel a los especialistas: autores de bibliotecas utilizadas para crear programas de aplicación.

Un ejemplo de construcción de una aplicación simple de subprocesos múltiples.

Nacido sobre el motivo de la gran cantidad de preguntas sobre la creación de aplicaciones de subprocesos múltiples en Delphi.

Objetivo este ejemplo- demostrar cómo construir correctamente una aplicación de subprocesos múltiples, con la eliminación del trabajo a largo plazo en un subproceso separado. Y cómo en una aplicación de este tipo garantizar la interacción del subproceso principal con el trabajador para transferir datos del formulario (componentes visuales) al subproceso y viceversa.

El ejemplo no pretende ser completo, solo demuestra lo más maneras simples interacciones de hilos. Permitiendo al usuario "deslumbrar rápidamente" (quién sabe cuánto me desagrada esto) una aplicación de subprocesos múltiples que se ejecuta correctamente.
Todo está detallado (en mi opinión) comentado en él, pero si tienes dudas, pregunta.
Pero una vez más te advierto: Los streams no son fáciles. Si no tiene idea de cómo funciona todo, existe un gran peligro de que a menudo todo funcione bien para usted y, a veces, el programa se comportará de manera más que extraña. El comportamiento de un programa de subprocesos múltiples escrito incorrectamente depende en gran medida de una gran cantidad de factores que a veces son imposibles de reproducir durante la depuración.

Así que un ejemplo. Para mayor comodidad, coloqué el código y adjunté el archivo con el código del módulo y el formulario.

unitExThreadForm;

usos
Windows, Mensajes, SysUtils, Variantes, Clases, Gráficos, Controles, Formularios,
Diálogos, StdCtrls;

// constantes utilizadas al pasar datos de un flujo a un formulario usando
// enviar mensajes de ventana
constante
WM_USER_SendMessageMetod = WM_USER+10;
WM_USER_PostMessageMetod = WM_USER+11;

escribe
// descripción de la clase thread, descendiente de tThread
tMiSubproceso = clase(tSubproceso)
privado
SincronizarDatosN:Entero;
SyncDataS:Cadena;
procedimiento SyncMethod1;
protegido
procedimiento Ejecutar; anular;
público
Param1:Cadena;
Param2:Entero;
Param3: Booleano;
Detenido: Booleano;
ÚltimoAleatorio:Entero;
Número de iteración: entero;
Lista de resultados: tStringList;

Constructor Create(aParam1:String);
destructor Destruir; anular;
fin;

// descripción de la clase del formulario que usa flujo
TForm1 = clase (TForm)
Etiqueta 1: Etiqueta T;
Memo1:TMemo;
btnIniciar: TButton;
btnParar: TButton;
Edición1: TEditar;
Edit2: TEdit;
CheckBox1: TCheckBox;
Etiqueta2: Etiqueta T;
Etiqueta 3: Etiqueta T;
Etiqueta 4: Etiqueta T;
procedimiento btnStartClick(Remitente: TObject);
procedimiento btnStopClick(Remitente: TObject);
privado
(Declaraciones privadas)
MiSubproceso:tMiSubproceso;
procedimiento EventMyThreadOnTerminate(Sender:tObject);
procedimiento EventOnSendMessageMetod (var Msg: TMessage); mensaje WM_USER_SendMessageMetod;
procedimiento EventOnPostMessageMetod(var Msg: TMessage); mensaje WM_USER_PostMessageMethod;

Público
(Declaraciones públicas)
fin;

variable
Formulario1: TForm1;

{
Detenido: demuestra el paso de datos de un formulario a un subproceso.
No requiere sincronización adicional, ya que es simple
tipo de palabra única, y está escrito por un solo hilo.
}

procedimiento TForm1.btnStartClick(Remitente: TObject);
comenzar
aleatorizar(); // asegurando la aleatoriedad en la secuencia por Random() - no tiene nada que ver con el hilo

// Crea una instancia del objeto de flujo, pasándole un parámetro de entrada
{
¡ATENCIÓN!
El constructor de subprocesos está escrito de tal manera que se crea el subproceso
suspendido porque permite:
1. Controlar el momento de su lanzamiento. Esto casi siempre es más conveniente, porque
le permite configurar una transmisión incluso antes del lanzamiento, pásela como entrada
parámetros, etc
2. Porque se guardará un enlace al objeto creado en el campo de formulario, luego
después de la autodestrucción del subproceso (ver más abajo) que, cuando el subproceso se está ejecutando
puede ocurrir en cualquier momento, este enlace dejará de ser válido.
}
MiSubproceso:= tMiSubproceso.Crear(Form1.Editar1.Texto);

// Sin embargo, dado que el subproceso se creó suspendido, cualquier error
// durante su inicialización (antes del lanzamiento), debemos destruirlo nosotros mismos
// por qué usar bloque try / except
tratar

// Asignación del manejador final del hilo en el que recibiremos
// resultados del hilo y "sobrescribir" el enlace a él
MiSubproceso.AlTerminar:= EventoMiSubprocesoAlTerminar;

// Dado que recopilaremos los resultados en OnTerminate, es decir antes de la autodestrucción
// subproceso, luego eliminaremos las preocupaciones de destruirlo
MiSubproceso.FreeOnTerminate:= Verdadero;

// Un ejemplo de paso de parámetros de entrada a través de los campos del objeto de flujo, en el punto
// crea una instancia cuando aún no se está ejecutando.
// Personalmente, prefiero hacer esto a través de los parámetros de override
// constructor (tMiSubproceso.Crear)
MiSubproceso.Param2:= StrToInt(Form1.Edit2.Text);

MiSubproceso.Detenido:= Falso; // algo así como un parámetro, pero cambiando durante
// tiempo de ejecución del hilo
excepto
// dado que el hilo aún no se está ejecutando y no podrá autodestruirse, lo destruiremos "manualmente"
LibreYNil(MiSubproceso);
// y luego dejar que la excepción se maneje de la manera habitual
aumentar;
fin;

// Dado que el objeto de subproceso se ha creado y configurado con éxito, es hora de ejecutarlo
MiSubproceso.Reanudar;

ShowMessage("Subproceso iniciado");
fin;

procedimiento TForm1.btnStopClick(Remitente: TObject);
comenzar
// Si la instancia del subproceso aún existe, pídale que se detenga
// Además, es "preguntar". También podemos "forzar" en principio, pero será
// exclusivamente una opción de emergencia, que requiere una comprensión clara de todo esto
// cocina en streaming. Por lo tanto, no se considera aquí.
si está asignado (Mi Subproceso) entonces
MiSubproceso.Detenido:= Verdadero
demás
ShowMessage("¡El subproceso no se está ejecutando!");
fin;

procedimiento TForm1.EventOnSendMessageMetod(var Msg: TMessage);
comenzar
// método de procesamiento de mensajes sincrónicos
// en WParam la dirección del objeto tMyThread, en LParam el valor actual del LastRandom del hilo
con tMyThread(Msg.WParam) comience
Form1.Label3.Caption:= Formato("%d %d %d",);
fin;
fin;

procedimiento TForm1.EventOnPostMessageMetod(var Msg: TMessage);
comenzar
// método de procesamiento de mensajes asíncronos
// en WParam el valor actual de IterationNo, en LParam el valor actual de LastRandom del hilo
Form1.Label4.Caption:= Formato("%d %d",);
fin;

procedimiento TForm1.EventMyThreadOnTerminate(Sender:tObject);
comenzar
// ¡IMPORTANTE!
// El método de manejo de eventos OnTerminate siempre se llama en el contexto de la principal
// subproceso: esto está garantizado por la implementación de tThread. Por lo tanto, es posible libremente
// usa cualquier propiedad y método de cualquier objeto

// Por si acaso, asegúrese de que la instancia del objeto aún exista
si no está asignado (MyThread), entonces Salir; // si no existe, entonces no hay nada que hacer

// obtener los resultados del trabajo del subproceso de la instancia del objeto subproceso
Form1.Memo1.Lines.Add(Format("Subproceso terminado con resultado %d",));
Form1.Memo1.Lines.AddStrings((Sender as tMyThread).ResultList);

// Destruye la referencia a la instancia del objeto de flujo.
// Porque el hilo se autodestruye (FreeOnTerminate:= True)
// entonces, después de la finalización del controlador OnTerminate, la instancia del objeto hilo será
// destruido (Gratis), y todas las referencias a él dejarán de ser válidas.
// Para no toparse accidentalmente con dicho enlace, deje que MyThread
// Lo anotaré nuevamente: no destruyamos el objeto, solo borre el enlace. Un objeto
// ¡destruirse a sí mismo!
Mi Subproceso: = Nil;
fin;

constructor tMyThread.Create(aParam1:String);
comenzar
// Crear una instancia del hilo SUSPENDIDO (ver comentario al instanciar)
heredadoCrear(Verdadero);

// Crear objetos internos (si es necesario)
Lista de resultados: = tStringList.Create;

// Obteniendo datos iniciales.

// Copiar los datos de entrada pasados ​​a través del parámetro
Param1:= aParam1;

// Un ejemplo de recepción de datos de entrada de componentes VCL en el constructor de un objeto de flujo
// Esto está permitido en este caso, ya que el constructor se llama en el contexto
// Hilo principal. Por lo tanto, se puede acceder a los componentes de VCL aquí.
// Pero, no me gusta esto, porque creo que es malo cuando el hilo sabe algo
// sobre alguna forma allí. Pero, ¿qué se puede hacer para demostrar.
Param3:= Form1.CheckBox1.Checked;
fin;

destructor tMyThread.Destroy;
comenzar
// destrucción de objetos internos
LibreYNil(ListaResultados);
// destruir el tThread subyacente
heredado;
fin;

procedimiento tMiSubproceso.Ejecutar;
variable
t:cardenal;
s:cadena;
comenzar
Número de iteración: = 0; // contador de resultados (número de bucle)

// En mi ejemplo, el cuerpo del hilo es un ciclo que termina
// o en la "solicitud" externa para finalizar pasada a través del parámetro variable Detenido,
// o simplemente haciendo 5 ciclos
// Es más agradable para mí escribir esto a través de un bucle "eterno".

Mientras que True comienza

Inc(IteraciónNo); // numero del siguiente ciclo

ÚltimoAleatorio:= Aleatorio(1000); // número aleatorio: para demostrar cómo pasar parámetros del hilo al formulario

T:= Aleatorio(5)+1; // tiempo por el cual nos quedaremos dormidos si no estamos completos

// Trabajo tonto (dependiendo del parámetro de entrada)
si no es Param3 entonces
Inc(Param2)
demás
Dec(Param2);

// Generar un resultado intermedio
s:= Formato("%s %5d %s %d %d",
);

// Agrega un resultado intermedio a la lista de resultados
ResultList.Add(s);

//// Ejemplos de pasar el resultado intermedio al formulario

//// Pasando por un método sincronizado - la forma clásica
//// Defectos:
//// - método sincronizado - este suele ser un método de clase de hilo (para acceder
//// a los campos del objeto stream), pero para acceder a los campos del formulario, debe
//// "saber" sobre él y sus campos (objetos), lo que no suele ser muy bueno con
//// punto de vista de la organización del programa.
//// - el hilo actual se suspenderá hasta que se complete la ejecución
//// método sincronizado.

//// Ventajas:
//// - estándar y universal
//// - en un método sincronizado, puede usar
//// todos los campos del objeto de flujo.
// primero, si es necesario, guarde los datos transmitidos en
// campos especiales del objeto objeto.
SyncDataN:=IteraciónNo;
SyncDataS:="Sync"+s;
// y luego asegurar una llamada de método sincronizada
Sincronizar (SyncMethod1);

//// Transmisión mediante envío de mensajes sincrónicos (SendMessage)
//// en este caso, los datos se pueden pasar tanto a través de los parámetros del mensaje (LastRandom),
//// ya través de los campos del objeto, pasando la dirección de la instancia en el parámetro del mensaje
//// Objeto Thread - Integer(Self).
//// Defectos:
//// - el hilo debe conocer el identificador de la ventana del formulario
//// - al igual que con Synchronize, el subproceso actual se suspenderá hasta
//// terminar de procesar el mensaje por el hilo principal
//// - requiere un tiempo de CPU significativo para cada llamada
//// (para cambiar hilos) por lo que una llamada muy frecuente no es deseable
//// Ventajas:
//// - al igual que con Synchronize, al procesar un mensaje, puede usar
//// todos los campos del objeto de flujo (a menos, por supuesto, que se haya pasado su dirección)


//// iniciar el hilo.
SendMessage(Form1.Handle,WM_USER_SendMessageMetod,Integer(Self),LastRandom);

//// Transmisión mediante envío de mensajes asíncronos (PostMessage)
//// Dado que en este caso, en el momento en que el hilo principal recibe el mensaje,
//// es posible que el hilo de envío ya haya terminado, pasando la dirección de la instancia
//// ¡el objeto del hilo no es válido!
//// Defectos:
//// - el hilo debe conocer el identificador de la ventana del formulario;
//// - debido a la asincronía, la transferencia de datos solo es posible a través de parámetros
//// mensajes, lo que complica significativamente la transferencia de datos que tienen el tamaño
//// más de dos palabras de máquina. Es conveniente usarlo para pasar enteros, etc.
//// Ventajas:
//// - a diferencia de los métodos anteriores, el hilo actual NO
//// suspendido, e inmediatamente continuará su ejecución
//// - a diferencia de una llamada sincronizada, un controlador de mensajes
//// es un método de formulario que debe tener conocimiento del objeto hilo,
//// o no saber nada sobre la transmisión si los datos solo se transfieren
//// a través de los parámetros del mensaje. Es decir, es posible que el hilo no sepa nada sobre el formulario.
//// en general, solo su Handle, que se puede pasar como parámetro antes
//// iniciar el hilo.
PostMessage(Form1.Handle,WM_USER_PostMessageMetod,IterationNo,LastRandom);

//// Comprobación de posible finalización

// Comprobación de finalización por parámetro
si se detiene, se rompe;

// Comprobar finalización por ocasión
si IterationNo >= 10 entonces Break;

dormir (t*1000); // Dormir por t segundos
fin;
fin;

procedimiento tMiSubproceso.SyncMethod1;
comenzar
// este método se llama a través del método Synchronize.
// Es decir, a pesar de que es un método del hilo tMyThread,
// se ejecuta en el contexto del subproceso principal de la aplicación.
// Por lo tanto, puede hacer todo, bueno, o casi todo :)
// Pero recuerda, no deberías "perder el tiempo" aquí por mucho tiempo

// Parámetros pasados, podemos extraerlos del campo especial donde los ponemos
// guardado antes de la llamada.
Form1.Label1.Caption:= SyncDataS;

// o de otros campos del objeto de flujo, por ejemplo, reflejando su estado actual
Form1.Label2.Caption:= Formato("%d %d",);
fin;

En general, el ejemplo fue precedido por el siguiente razonamiento mío sobre el tema...

Primeramente:
LA REGLA MÁS IMPORTANTE Programación multiproceso en Delphi:
En el contexto de un subproceso no principal, es imposible acceder a las propiedades y métodos de los formularios y, de hecho, a todos los componentes que "crecen" desde tWinControl.

Esto significa (ligeramente simplificado) que ni en el método Execute heredado de TThread, ni en otros métodos/procedimientos/funciones llamados desde Execute, esta prohibido acceder directamente a cualquier propiedad y método de los componentes visuales.

Cómo hacerlo bien.
No hay recetas únicas aquí. Más precisamente, hay tantas y diferentes opciones que, según el caso específico, debe elegir. Por lo tanto, se refieren al artículo. Después de leerlo y comprenderlo, el programador podrá comprender cuál es la mejor manera de actuar en tal o cual caso.

En una palabra:

La mayoría de las veces, una aplicación se vuelve multiproceso cuando es necesario realizar algún tipo de trabajo a largo plazo o cuando es posible hacer varias cosas simultáneamente que no cargan mucho el procesador.

En el primer caso, la implementación del trabajo dentro del hilo principal conduce a la "ralentización" de la interfaz de usuario: mientras se realiza el trabajo, el ciclo de procesamiento de mensajes no se ejecuta. Como resultado, el programa no responde a las acciones del usuario y el formulario no se dibuja, por ejemplo, después de que el usuario lo haya movido.

En el segundo caso, cuando el trabajo implica un intercambio activo con mundo exterior, luego durante el "tiempo de inactividad" forzado. Antes de recibir/enviar datos, puede hacer otra cosa en paralelo, por ejemplo, enviar/recibir datos nuevamente.

Hay otros casos, pero menos comunes. Sin embargo, esto no es importante. Ahora no se trata de eso.

Ahora como se escribe. Naturalmente, se considera un cierto caso más frecuente, algo generalizado. Entonces.

El trabajo realizado en un hilo separado, en el caso general, tiene cuatro entidades (ni siquiera sé cómo llamarlo con más precisión):
1. Datos iniciales
2. El trabajo real en sí (puede depender de los datos de origen)
3. Datos intermedios (por ejemplo, información sobre el estado actual del trabajo que se está realizando)
4. Salida (resultado)

Muy a menudo, los componentes visuales se utilizan para leer y mostrar la mayoría de los datos. Pero, como se mencionó anteriormente, no puede acceder directamente a los componentes visuales desde un hilo. ¿Cómo ser?
Los desarrolladores de Delphi sugieren usar el método Synchronize de la clase TThread. Aquí no describiré cómo aplicarlo; para esto está el artículo anterior. Permítanme decir que su uso, incluso el correcto, no siempre está justificado. Hay dos problemas:

En primer lugar, el cuerpo de un método llamado a través de Synchronize siempre se ejecuta en el contexto del subproceso principal y, por lo tanto, mientras se ejecuta, el bucle de procesamiento de mensajes de la ventana no se ejecuta nuevamente. Por lo tanto, debe ser rápido, de lo contrario, obtendremos los mismos problemas que con una implementación de un solo subproceso. Idealmente, un método llamado a través de Synchronize generalmente solo debería usarse para acceder a propiedades y métodos de objetos visuales.

En segundo lugar, ejecutar un método a través de Synchronize es un placer "caro", causado por la necesidad de dos cambios entre subprocesos.

Además, ambos problemas están interrelacionados y provocan una contradicción: por un lado, para resolver el primero, es necesario “triturar” los métodos llamados a través de Synchronize, y por otro lado, luego hay que llamarlos más a menudo, desperdiciando preciosos recursos del procesador.

Por lo tanto, como siempre, es necesario abordar con prudencia, y para diferentes casos, utilizar diferentes caminos interacción de flujo con el mundo exterior:

Datos iniciales
Todos los datos que se transfieren a la transmisión y que no cambian durante su funcionamiento deben transferirse incluso antes de que comience, es decir. al crear un hilo. Para usarlos en el cuerpo de un hilo, debe hacer una copia local de ellos (generalmente en los campos de un descendiente de TThread).
Si hay datos iniciales que pueden cambiar durante la operación del subproceso, entonces el acceso a dichos datos debe realizarse a través de métodos sincronizados (métodos llamados a través de Synchronize), o mediante los campos del objeto del subproceso (descendiente de TThread). Esto último requiere cierta precaución.

Datos intermedios y de salida
Aquí, nuevamente, hay varias formas (en orden de mi preferencia):
- Un método para enviar mensajes de forma asíncrona a la ventana principal de la aplicación.
Suele utilizarse para enviar mensajes sobre el progreso de un proceso a la ventana principal de la aplicación, con una pequeña cantidad de datos (por ejemplo, porcentaje de finalización)
- Método de envío sincrónico de mensajes a la ventana principal de la aplicación.
Por lo general, se usa para los mismos propósitos que el envío asíncrono, pero le permite transferir una mayor cantidad de datos sin crear una copia separada.
- Métodos sincronizados, cuando sea posible, combinando la transferencia de tantos datos como sea posible en un solo método.
También se puede utilizar para obtener datos de un formulario.
- A través de los campos del objeto stream, proporcionando acceso mutuamente excluyente.
Más detalles se pueden encontrar en el artículo.

Eh Falló brevemente de nuevo.

Capítulo 10.

Aplicaciones de subprocesos múltiples

La multitarea en los sistemas operativos modernos se da por sentado [ Antes de la llegada de Apple OS X, no había sistemas operativos multitarea modernos en las computadoras Macintosh. Diseñar correctamente un sistema operativo con multitarea completa es muy difícil, por lo que OS X tuvo que basarse en el sistema Unix.]. El usuario espera que con el lanzamiento simultáneo editor de texto y el cliente de correo, estos programas no entrarán en conflicto, y al recibir Correo electrónico el editor no dejará de funcionar. Cuando se ejecutan varios programas al mismo tiempo, el sistema operativo cambia rápidamente entre programas, proporcionándoles un procesador a su vez (a menos, por supuesto, que se instalen varios procesadores en la computadora). Como resultado, crea espejismo ejecutar múltiples programas al mismo tiempo, porque incluso el mejor mecanógrafo (y la conexión a Internet más rápida) no puede mantenerse al día con un procesador moderno.

Multithreading, en cierto sentido, puede verse como el siguiente nivel de multitarea: en lugar de cambiar entre diferentes programas el sistema operativo cambia entre diferentes partes del mismo programa. Por ejemplo, multiproceso cliente de correo le permite recibir nuevos mensajes de correo electrónico mientras lee o redacta nuevos mensajes. Hoy en día, muchos usuarios también dan por sentado el multiproceso.

VB nunca ha tenido soporte normal para subprocesos múltiples. Es cierto que en VB5 apareció una de sus variedades: modelo de transmisión colaborativa(enhebrado de apartamentos). Como verá en breve, el modelo concurrente proporciona al programador algunos de los beneficios de los subprocesos múltiples, pero no los aprovecha al máximo. Tarde o temprano, debe cambiar de una máquina de entrenamiento a una real, y VB .NET se convirtió en la primera versión de VB compatible con un modelo gratuito de subprocesos múltiples.

Sin embargo, los subprocesos múltiples no son una característica que se implemente fácilmente en los lenguajes de programación y que los programadores dominen con facilidad. ¿Por qué?

Porque las aplicaciones de subprocesos múltiples pueden tener errores muy complicados que aparecen y desaparecen de manera impredecible (y esos errores son los más difíciles de depurar).

Advertencia justa: los subprocesos múltiples son una de las áreas más difíciles de la programación. La más mínima falta de atención conduce a la aparición de errores esquivos, cuya corrección requiere sumas astronómicas. Por esta razón, este capítulo contiene muchos malo ejemplos: los escribimos deliberadamente de tal manera que demostraran errores característicos. Este es el enfoque más seguro para aprender a programar con subprocesos múltiples: debería poder ver problemas potenciales cuando todo parece funcionar bien a primera vista y saber cómo resolverlos. Si desea utilizar las técnicas de programación multihilo, esto es indispensable.

Este capítulo sentará una base sólida para futuras Trabajo independiente, pero no podremos describir la programación de subprocesos múltiples en todas sus sutilezas: solo la documentación impresa sobre las clases del espacio de nombres Threading ocupa más de 100 páginas. Si desea dominar la programación de subprocesos múltiples a un nivel superior, consulte libros especializados.

Pero por muy peligrosa que sea la programación multiproceso, es indispensable para la solución profesional de algunos problemas. Si sus programas no utilizan subprocesos múltiples en su caso, los usuarios se sentirán muy decepcionados y preferirán otro producto. Por ejemplo, solo en la cuarta versión del popular programa de correo electrónico Eudora aparecieron capacidades de subprocesos múltiples, sin las cuales es imposible imaginar cualquier programa moderno para trabajar con el correo electrónico. Cuando Eudora introdujo la compatibilidad con subprocesos múltiples, muchos usuarios (incluido uno de los autores de este libro) se habían cambiado a otros productos.

Finalmente, los programas de subproceso único simplemente no existen en .NET. Todo Los programas .NET son multiproceso porque el recolector de elementos no utilizados se ejecuta como un proceso en segundo plano de baja prioridad. Como se muestra a continuación, en la programación gráfica seria en .NET, la comunicación adecuada de los subprocesos del programa ayuda a evitar que la GUI se bloquee cuando un programa realiza operaciones prolongadas.

Introducción a los subprocesos múltiples

Cada programa trabaja en un cierto contexto, describe la distribución de código y datos en la memoria. Cuando se guarda el contexto, el estado del subproceso del programa se guarda realmente, lo que le permite restaurarlo en el futuro y continuar con la ejecución del programa.

Guardar el contexto está asociado con un cierto costo de tiempo y memoria. El sistema operativo recuerda el estado de un hilo de programa y transfiere el control a otro hilo. Cuando el programa quiere continuar ejecutando el hilo suspendido, el contexto guardado debe restaurarse, lo que lleva aún más tiempo. Por lo tanto, los subprocesos múltiples solo deben usarse cuando los beneficios superan los costos. A continuación se enumeran algunos ejemplos típicos.

  • La funcionalidad del programa se divide clara y naturalmente en varias operaciones heterogéneas, como en el ejemplo de recibir correo electrónico y preparar nuevos mensajes.
  • El programa realiza cálculos largos y complejos, y no desea que la interfaz gráfica se bloquee durante los cálculos.
  • El programa se ejecuta en una computadora multiprocesador con un sistema operativo que admite el uso de múltiples procesadores (siempre que la cantidad de subprocesos activos no exceda la cantidad de procesadores, la ejecución en paralelo casi no cuesta costos de cambio de subprocesos).

Antes de pasar a la mecánica de funcionamiento de los programas multihilo, es necesario señalar una circunstancia que suele causar confusión entre los principiantes en el campo de la programación multihilo.

El hilo del programa ejecutará el procedimiento, no el objeto.

Es difícil decir qué significa la frase "se está ejecutando un objeto", pero uno de los autores a menudo imparte seminarios sobre programación multiproceso y esta pregunta se hace con más frecuencia que otras. Algunos podrían pensar que un subproceso de programa comienza llamando al método New de una clase, después de lo cual el subproceso procesa todos los mensajes pasados ​​al objeto correspondiente. Tales representaciones absolutamente estan equivocados. Un objeto puede contener varios subprocesos que ejecutan métodos diferentes (y a veces incluso los mismos), mientras que los mensajes de objetos son transmitidos y recibidos por varios subprocesos diferentes (por cierto, esta es una de las razones por las que la programación multiproceso es difícil: para depurar un programa, ¡necesita saber qué subproceso en un momento dado realiza este o aquel procedimiento!).

Debido a que los subprocesos se crean sobre la base de métodos de objetos, el objeto en sí generalmente se crea antes que el subproceso. Después de crear con éxito un objeto, el programa crea un hilo, pasándole la dirección del método del objeto, y solo despues de eso indica al subproceso que comience a ejecutarse. El procedimiento para el que se creó el subproceso, como todos los procedimientos, puede crear nuevos objetos, realizar operaciones en objetos existentes y llamar a otros procedimientos y funciones que están en su ámbito.

Los subprocesos del programa también pueden ejecutar métodos de clase compartida. En este slu-También hay que tener en cuenta otra circunstancia importante: el hilo termina con la salida del procedimiento para el que fue creado. Hasta la salida del procedimiento, la finalización normal del hilo del programa es imposible.

Los subprocesos pueden terminar no solo de forma natural, sino también de forma anormal. Por lo general, esto no se recomienda. Consulte Terminación e interrupción de subprocesos para obtener más información.

Las funciones principales de .NET relacionadas con el uso de subprocesos se encuentran en el espacio de nombres Threading. Por lo tanto, la mayoría de los programas de subprocesos múltiples deben comenzar con la siguiente línea:

Importaciones Sistema.Roscado

La importación de un espacio de nombres simplifica la entrada del programa y le permite usar la tecnología IntelliSense.

La conexión directa de los hilos con los procedimientos sugiere que en este cuadro un lugar importante lo ocupan delegados(ver capítulo 6). En particular, el espacio de nombres Threading incluye el delegado ThreadStart, comúnmente utilizado al iniciar subprocesos de programa. La sintaxis para usar este delegado se ve así:

Subproceso de delegado público ()

El código llamado con el delegado ThreadStart no debe tener parámetros ni un valor de retorno, por lo que no se pueden crear subprocesos para funciones (que devuelven un valor) y procedimientos con parámetros. Para pasar información de un flujo, también hay que buscar medios alternativos, ya que los métodos que se ejecutan no devuelven valores y no pueden usar el paso por referencia. Por ejemplo, si el procedimiento ThreadMethod está en la clase WilluseThread, ThreadMethod puede comunicar información cambiando las propiedades de las instancias de la clase WillUseThread.

Dominios de aplicación

Los subprocesos del programa .NET se ejecutan en los llamados dominios de aplicación, definidos en la documentación como "un entorno aislado en el que se ejecuta una aplicación". Puede pensar en un dominio de aplicación como una versión ligera de los procesos de Win32; un único proceso de Win32 puede contener varios dominios de aplicación. La principal diferencia entre los dominios de aplicación y los procesos es que un proceso Win32 tiene su propio espacio de direcciones (la documentación también compara los dominios de aplicación con los procesos lógicos que se ejecutan dentro de un proceso físico). En .NET, toda la gestión de la memoria está a cargo del tiempo de ejecución, por lo que varios dominios de aplicaciones pueden ejecutarse en el mismo proceso de Win32. Uno de los beneficios de este esquema es mejorar las capacidades de escalado de las aplicaciones. Las herramientas para trabajar con dominios de aplicación están en la clase AppDomain. Le recomendamos que lea la documentación de esta clase. Con él, puede obtener información sobre el entorno en el que se ejecuta su programa. En particular, la clase AppDomain se usa al reflexionar sobre las clases del sistema .NET. El siguiente programa enumera los ensamblajes cargados.

Sistema de Importaciones.Reflexión

Módulo

Sub principal()

Dim theDomain como AppDomain

theDomain = AppDomain.CurrentDomain

Ensamblajes Dim () como

Asambleas = theDomain.GetAssemblies

Dim anAssemblyxAs

Para cada una Asamblea en asambleas

Console.WriteLinetanAssembly.Nombre completo) Siguiente

Consola.ReadLine()

final sub

Módulo final

Creando hilos

Comencemos con un ejemplo elemental. Digamos que desea ejecutar un procedimiento en un subproceso separado que reduce un contador en un ciclo sin fin. El procedimiento se define como parte de la clase:

Clase pública WillUseThreads

Sustracción pública SubtractFromCounter()

Dim cuenta como entero

Hacer mientras es cierto contar -= 1

Console.WriteLlne("Estoy en otro hilo y contador ="

&contar)

círculo

final sub

clase final

Dado que la condición del bucle Do siempre es verdadera, puede pensar que nada detendrá la ejecución del procedimiento SubtractFromCounter. Sin embargo, este no es siempre el caso en una aplicación de subprocesos múltiples.

El siguiente fragmento muestra el procedimiento Sub Main que inicia el subproceso y el comando Imports:

Opción estricta en el sistema de importaciones.Módulo de módulo de roscado

Sub principal()

1 Dim myTest como nuevo WillUseThreads()

2 Dim bThreadStart como nuevo ThreadStart(AddressOf _

myTest.SubtractFromCounter)

3 Dim bThread como nuevo hilo (bThreadStart)

4" bRosca.Inicio()

Dim i como entero

5 Hacer mientras sea cierto

Console.WriteLine("En el hilo principal y el conteo es " & i) i += 1

círculo

final sub

Módulo final

Echemos un vistazo a los puntos más importantes uno por uno. En primer lugar, el procedimiento Sub Man n siempre funciona en convencional(Hilo principal). Siempre hay al menos dos subprocesos ejecutándose en los programas .NET: el subproceso principal y el subproceso de recolección de elementos no utilizados. La línea 1 crea una nueva instancia de la clase de prueba. En la línea 2, creamos un delegado ThreadStart y pasamos la dirección del procedimiento SubtractFromCounter de la instancia de clase de prueba creada en la línea 1 (este procedimiento se llama sin parámetros). BienSe puede omitir dar a la importación del espacio de nombres Threading un nombre largo. El nuevo objeto de subproceso se crea en la línea 3. Observe el paso del delegado ThreadStart al llamar al constructor de la clase Subproceso. Algunos programadores prefieren combinar estas dos líneas en una línea lógica:

Dim bThread como nuevo hilo (New ThreadStarttAddressOf _

myTest.SubtractFromCounter))

Finalmente, la línea 4 "inicia" el hilo llamando al método Start de la instancia de Thread creada para el delegado ThreadStart. Al llamar a este método, le indicamos al sistema operativo que el procedimiento Restar debe ejecutarse en un subproceso separado.

La palabra "comienza" en el párrafo anterior está entre comillas, porque en este caso ocurre una de las muchas rarezas de la programación multihilo: ¡llamar a Start en realidad no inicia el hilo! Simplemente dice que el sistema operativo debe programar la ejecución del subproceso especificado, pero ejecutarlo directamente está fuera del control del programa. No podrá iniciar hilos por su cuenta, porque el sistema operativo siempre gestiona la ejecución de los hilos. En una sección posterior, aprenderá cómo usar la prioridad para hacer que el sistema operativo ejecute su subproceso más rápido.

En la fig. La figura 10.1 muestra un ejemplo de lo que puede suceder después de que se inicia un programa y luego se interrumpe con la tecla Ctrl+Break. En nuestro caso, ¡el nuevo hilo comenzó solo después de que el contador en el hilo principal aumentara a 341!

Arroz. 10.1. Tiempo de ejecución programático simple de subprocesos múltiples

Si el programa se ejecuta durante un período de tiempo más largo, el resultado se parecerá al que se muestra en la Fig. 10.2. vemos que tula ejecución del subproceso en ejecución se suspende y el control se transfiere nuevamente al subproceso principal. En este caso, hay una manifestación. subprocesos múltiples preventivos a través de cortes de tiempo. El significado de este aterrador término se explica a continuación.

Arroz. 10.2. Cambiar entre subprocesos en un programa multiproceso simple

Al interrumpir subprocesos y transferir el control a otros subprocesos, el sistema operativo utiliza el principio de subprocesos múltiples preventivos a través de la división del tiempo. La división de tiempo también resuelve uno de los problemas comunes que solían ocurrir en los programas de subprocesos múltiples: un subproceso ocupa todo el tiempo de la CPU y no cede el control a otros subprocesos (generalmente esto sucede en bucles intensivos como el anterior). Para evitar la toma de control exclusiva de la CPU, sus subprocesos deben transferir el control a otros subprocesos de vez en cuando. Si el programa resulta ser "no consciente", hay otra solución, un poco menos deseable: el sistema operativo siempre se adelanta a un subproceso en ejecución, independientemente de su nivel de prioridad, de modo que el acceso al procesador se otorga a cada subproceso en el sistema.

Dado que los esquemas de división de todas las versiones de Windows que ejecutan .NET asignan una cantidad mínima de tiempo a cada subproceso, los problemas de propiedad de la CPU no son tan graves en la programación de .NET. Por otro lado, si alguna vez se adapta el entorno .NET para otros sistemas, la situación puede cambiar.

Si incluimos la siguiente línea en nuestro programa antes de llamar a Inicio, incluso los subprocesos con la prioridad más baja obtendrán una parte del tiempo de CPU:

bThread.Priority = ThreadPriority.Highest

Arroz. 10.3. El hilo de mayor prioridad generalmente comienza más rápido

Arroz. 10.4. El procesador también se asigna a subprocesos de menor prioridad.

El comando asigna la máxima prioridad al nuevo hilo y disminuye la prioridad del hilo principal. De la fig. La Figura 10.3 muestra que el nuevo subproceso comienza a ejecutarse más rápido que antes, pero como muestra la Figura 10-3. 10.4, el hilo principal también recibe controlleniya (aunque muy brevemente y solo después trabajo continuo flujo con resta). Cuando ejecute el programa en sus computadoras, obtendrá resultados similares a los que se muestran en la Fig. 10.3 y 10.4, pero debido a las diferencias entre nuestros sistemas, no habrá una coincidencia exacta.

El tipo enumerado ThreadPrlority incluye valores para cinco niveles de prioridad:

ThreadPriority.Highest

ThreadPriority.AboveNormal

Subproceso Prioridad.Normal

ThreadPriority.BelowNormal

ThreadPriority.Lowest

método de unión

A veces, un subproceso de programa debe suspenderse hasta que se complete otro subproceso. Digamos que desea suspender el subproceso 1 hasta que el subproceso 2 haya terminado su cálculo. Para esto de la secuencia 1 se llama al método Join para el subproceso 2. En otras palabras, el comando

subproceso2.Unirse()

suspende el subproceso actual y espera a que se complete el subproceso 2. El subproceso 1 pasa a estado bloqueado.

Si une el subproceso 1 con el subproceso 2 utilizando el método Join, el sistema operativo iniciará automáticamente el subproceso 1 después de que finalice el subproceso 2. Tenga en cuenta que el proceso de inicio es no determinista: no hay forma de saber exactamente cuánto tiempo después de que termine el hilo 2, comenzará a funcionar el hilo 1. Hay otra versión de Join que devuelve un valor booleano:

thread2.Join(Entero)

Este método espera a que se complete el subproceso 2 o desbloquea el subproceso 1 después de que haya transcurrido el intervalo de tiempo especificado, lo que hace que el programador del sistema operativo reasigne el tiempo del procesador al subproceso. El método devuelve True si el subproceso 2 finaliza antes de que caduque el intervalo de tiempo de espera especificado y False en caso contrario.

No olvide la regla básica: si el subproceso 2 finalizó o se agotó el tiempo de espera, no puede controlar cuándo se activa el subproceso 1.

Nombres de subprocesos, CurrentThread y ThreadState

La propiedad Thread.CurrentThread devuelve una referencia al objeto de subproceso que se está ejecutando actualmente.

Aunque VB .NET tiene una gran ventana Subprocesos para depurar aplicaciones de subprocesos múltiples, que se describe a continuación, a menudo hemos acudido al rescate con el comando

MsgBox(Subproceso.SubprocesoActual.Nombre)

A menudo resultó que el código se ejecuta en un hilo completamente diferente, en el que se suponía que debía ejecutarse.

Recuerde que el término "programación no determinista de subprocesos de programa" significa algo muy simple: el programador prácticamente no tiene medios a su disposición para influir en el trabajo del programador. Por esta razón, los programas suelen utilizar la propiedad ThreadState, que devuelve información sobre el estado actual del hilo.

Ventana Subprocesos

La ventana Subprocesos de Visual Studio .NET es una ayuda invaluable para depurar programas de subprocesos múltiples. Se activa con el comando del submenú Depurar > Windows en modo de interrupción. Digamos que nombró el subproceso bThread con el siguiente comando:

bThread.Name = "Restar hilo"

En la Fig. 10.5.

Arroz. 10.5. Ventana Subprocesos

La flecha de la primera columna marca el subproceso activo devuelto por la propiedad Thread.CurrentThread. La columna ID contiene los ID numéricos de subprocesos. La siguiente columna enumera los nombres de los subprocesos (si se ha asignado alguno). La columna Ubicación indica el procedimiento a ejecutar (por ejemplo, el procedimiento WriteLine de la clase Consola en la Figura 10-5). Las columnas restantes contienen información sobre prioridad y subprocesos suspendidos (consulte la siguiente sección).

La ventana de subprocesos (¡no el sistema operativo!) le permite controlar los subprocesos de su programa usando menús contextuales. Por ejemplo, puede detener el hilo actual haciendo clic en la línea correspondiente botón derecho del ratón mouse y seleccione el comando Congelar (luego se puede reanudar el trabajo del hilo detenido). La detención de subprocesos se usa a menudo durante la depuración para que un subproceso que se comporta mal no interfiera con la aplicación. Además, la ventana de hilos le permite activar otro hilo (no detenido); para hacer esto, haga clic con el botón derecho en la línea deseada y seleccione el comando Cambiar a hilo en el menú contextual (o simplemente haga doble clic en la línea del hilo). Como se mostrará más adelante, esto es muy útil para diagnosticar posibles interbloqueos.

Suspendiendo un hilo

Los subprocesos no utilizados temporalmente se pueden poner en un estado pasivo mediante el método de suspensión. Un subproceso pasivo también se considera bloqueado. Por supuesto, con la transferencia del subproceso al estado pasivo, la parte de los subprocesos restantes obtendrá más recursos de procesador. La sintaxis estándar para el método Sleeper es la siguiente: Thread.Sleep(interval_in_milliseconds)

Llamar a Sleep hace que el subproceso activo se vuelva inactivo durante al menos el número especificado de milisegundos (sin embargo, no se garantiza que se active inmediatamente después de que haya transcurrido el intervalo especificado). Tenga en cuenta que cuando se llama al método, no se pasa ninguna referencia a un subproceso específico: el método Sleep se llama solo para el subproceso activo.

Otra versión de Sleep hace que el subproceso actual produzca el resto del tiempo de CPU asignado:

Subproceso.Sueño(0)

La siguiente opción pone el subproceso actual en un estado pasivo por un tiempo ilimitado (la activación ocurre solo cuando se llama a Interrupción):

Subproceso.Sleer(Tiempo de espera.Infinito)

Debido a que los subprocesos pasivos (incluso con tiempos de espera ilimitados) pueden ser interrumpidos por el método Interrupt, lo que provoca que se lance una ThreadInterruptExceptionException, la llamada Slayer siempre se envuelve en un bloque Try-Catch, como en el siguiente fragmento:

tratar

Subproceso.Sueño(200)

"El estado del subproceso pasivo ha sido abortado

Atrapar e como excepción

"Otras excepciones

Finalizar intento

Cada programa .NET se ejecuta en un subproceso de programa, por lo que el método Sleep también se usa para suspender programas (si el programa no importa el espacio de nombres Threadipg, debe usar el nombre completo Threading.Thread.Sleep).

Terminación o interrupción de subprocesos del programa

El subproceso finaliza automáticamente al salir del método especificado cuando se creó el delegado ThreadStart, pero a veces es conveniente finalizar el método (por lo tanto, el subproceso) cuando se producen ciertos factores. En tales casos, las secuencias suelen comprobar variable de condición dependiendo del estado dese toma una decisión sobre una salida de emergencia del arroyo. Como regla general, se incluye un bucle Do-While en el procedimiento para esto:

Sub ThreadedMethod()

"El programa debe proporcionar medios para encuestar

"Variable de condición.

" Por ejemplo, una variable de condición se puede escribir como una propiedad

Do While conditionVariable = False y MoreWorkToDo

" Código principal

Sub de final de bucle

Lleva algún tiempo sondear la variable de condición. El sondeo constante en una condición de bucle solo debe usarse si espera que el subproceso finalice prematuramente.

Si la prueba de la variable de condición debe ocurrir en una ubicación específica, use el comando If-Then en combinación con Exit Sub dentro de un ciclo infinito.

El acceso a una variable de condición debe sincronizarse para que otros subprocesos no interfieran con su uso normal. Este importante tema se trata en la sección Solución de problemas: Sincronización.

Desafortunadamente, el código de los subprocesos pasivos (o bloqueados) no se ejecuta, por lo que sondear una variable condicional no es una opción para ellos. En este caso, debe llamar al método de interrupción en una variable de objeto que contenga una referencia al hilo deseado.

El método de interrupción solo se puede llamar en subprocesos que están en estado de espera, suspensión o unión. Si llama a Interrupt en un subproceso que se encuentra en uno de estos estados, luego de un tiempo, el subproceso comenzará a ejecutarse nuevamente y el tiempo de ejecución generará una excepción ThreadInterruptedException en el subproceso. Esto ocurre incluso si el subproceso se ha puesto a dormir indefinidamente llamando a Thread.Sleepdimeout. infinito). Decimos "después de un tiempo" porque la programación de subprocesos no es determinista por naturaleza. La excepción ThreadInterruptedException es capturada por la sección Catch que contiene el código de salida del estado de espera. Sin embargo, la sección Catch no es necesaria para terminar el subproceso llamando a Interrupción: el subproceso maneja la excepción como mejor le parezca.

En .NET, el método de Interrupción se puede llamar incluso en subprocesos sin bloqueo. En este caso, el hilo se interrumpe en el siguiente bloque.

Pausar y matar hilos

El espacio de nombres Threading contiene otros métodos que interrumpen el funcionamiento normal de los hilos:

  • Suspender;
  • aborto.

Es difícil decir por qué .NET incluye soporte para estos métodos: llamar a Suspend and Abort probablemente hará que el programa se vuelva inestable. Ninguno de los métodos le permite desinicializar correctamente el hilo. Además, cuando se llama a Suspend o Abort, es imposible predecir en qué estado el subproceso dejará los objetos después de ser suspendido o abortado.

Llamar a Abort genera una excepción ThreadAbortException. Para ayudarlo a comprender por qué esta extraña excepción no debe manejarse en los programas, aquí hay un extracto de la documentación de .NET SDK:

“...Cuando se destruye un subproceso llamando a Abort, el tiempo de ejecución lanza una ThreadAbortException. Este es un tipo especial de excepción que no puede ser capturado por el programa. Cuando se lanza esta excepción, el tiempo de ejecución ejecuta todos los bloques Finalmente antes de eliminar el subproceso. Debido a que los bloques finalmente pueden hacer cualquier cosa, llame a Join para asegurarse de que el hilo se elimine".

Moraleja: no se recomiendan Abort y Suspend (y si aún no puede prescindir de Suspend, reanude el hilo suspendido con el método Resume). La única forma de terminar un subproceso de manera segura es sondeando una variable de condición sincronizada o llamando al método Interrupt discutido anteriormente.

Subprocesos de fondo (demonios)

Algunos subprocesos que se ejecutan en segundo plano finalizan automáticamente cuando se detienen otros componentes del programa. En particular, el recolector de elementos no utilizados se ejecuta en uno de los subprocesos de fondo. Por lo general, los subprocesos en segundo plano se crean para recibir datos, pero esto solo se hace si hay código ejecutándose en otros subprocesos que pueden procesar los datos recibidos. Sintaxis: nombre del subproceso.IsBackGround = True

Si solo quedan subprocesos en segundo plano en la aplicación, la aplicación finaliza automáticamente.

Un ejemplo más serio: extraer datos de código HTML

Recomendamos usar subprocesos solo cuando la funcionalidad del programa esté claramente dividida en varias operaciones. Un buen ejemplo es el programa de extracción de HTML del Capítulo 9. Nuestra clase hace dos cosas: obtener datos del sitio de Amazon y procesarlos. Este es un ejemplo perfecto de una situación en la que la programación de subprocesos múltiples es realmente apropiada. Creamos clases para varios libros diferentes y luego analizamos los datos en diferentes hilos. La creación de un nuevo hilo para cada libro mejora la eficiencia del programa porque mientras un hilo recibe datos (lo que puede requerir esperar en el servidor de Amazon), otro hilo estará ocupado procesando datos ya recibidos.

La variante de subprocesos múltiples de este programa funciona de manera más eficiente que la variante de un solo subproceso solo en una computadora con varios procesadores o si la recepción de datos adicionales se puede combinar de manera efectiva con su análisis.

Como se mencionó anteriormente, solo los procedimientos que no tienen parámetros pueden ejecutarse en subprocesos, por lo que deberá realizar pequeños cambios en el programa. El siguiente es el procedimiento principal, reescrito con la excepción de los parámetros:

FindRank público secundario ()

m_Rank = RasparAmazon()

Console.WriteLine("el rango de" & m_Name & "Is" & GetRank)

final sub

Dado que no podremos usar el campo combinado para almacenar y recuperar información (escribir programas de subprocesos múltiples con interfaz gráfica de usuario discutido en la última sección de este capítulo), el programa almacena los datos de cuatro libros en una matriz cuya definición comienza así:

Dim theBook(3.1) As String theBook(0.0) = "1893115992"

theBook(0.l) = "Programación VB .NET" " Etc.

Se crean cuatro subprocesos en el mismo ciclo que crea los objetos de AmazonRanker:

Para i= 0 entonces 3

tratar

theRanker = New AmazonRanker(theBook(i.0). theBookd.1))

aThreadStart = New ThreadStar(Dirección del Ranker.FindRan()

unSubproceso = Nuevo Subproceso(aSubprocesoInicio)

unHilo.Nombre = elLibro(i.l)

aThread.Start() Captura e como excepción

Console.WriteLine(e.Mensaje)

Finalizar intento

próximo

A continuación se muestra el texto completo del programa:

Opción Estricta en Importaciones System.IO Importaciones System.Net

Importaciones Sistema.Roscado

Módulo

Sub principal()

Dim theBook(3.1) como cadena

elLibro(0.0) = "1893115992"

theBook(0.l) = "Programación VB .NET"

elLibro(l.0) = "1893115291"

theBook(l.l) = "Programación de base de datos VB .NET"

elLibro(2,0) = "1893115623"

theBook(2.1) = "Introducción del programador a C#".

elLibro(3.0) = "1893115593"

theBook(3.1) = "Gland the .Net Platform"

Dim i como entero

Dim theRanker como =AmazonRanker

Dim aThreadStart como Threading.ThreadStart

Dim aThread como Threading.Thread

Para i = 0 a 3

tratar

theRanker = New AmazonRankerttheBook(i.0). elLibro(i.1))

aThreadStart = New ThreadStart(DireccióndelRanker.FindRank)

unSubproceso = Nuevo Subproceso(aSubprocesoInicio)

aThread.Name= theBook(i.l)

unHilo.Inicio()

Atrapar e como excepción

Console.WriteLlnete.Message)

Finalizar Probar Siguiente

Consola.ReadLine()

final sub

Módulo final

AmazonRanker de clase pública

m_URL privado como cadena

Privado m_Rank como entero

m_Name privado como cadena

Public Sub New (ByVal ISBN como cadena. ByVal theName como cadena)

m_URL = "http://www.amazon.com/exec/obidos/ASIN/" & ISBN

m_Name = theName End Sub

Público Sub FindRank() m_Rank = ScrapeAmazon()

Console.Writeline("el rango de " & m_Name & "es "

& GetRank) End Sub

Propiedad pública de solo lectura GetRank() como cadena Obtener

Si m_Rank<>0 Entonces

Devolver CStr(m_Rank) De lo contrario

" Problemas

Terminara si

Fin de obtener

Propiedad final

Propiedad pública de solo lectura GetName() como cadena Obtener

Devolver m_Name

Fin de obtener

Propiedad final

Función privada ScrapeAmazon() como entero Try

Dim theURL como nuevo Uri (m_URL)

Dim theRequest como WebRequest

theRequest =WebRequest.Create(laURL)

Dim theResponse como WebResponse

laRespuesta = laSolicitud.ObtenerRespuesta

Dim aReader como New StreamReader (theResponse.GetResponseStream())

Atenuar los datos como cadena

theData = aReader.ReadToEnd

Devolver Analizar (los Datos)

Captura E como excepción

Console.WriteLine(E.Mensaje)

Console.WriteLine(E.StackTrace)

consola. LeerLínea()

Finalizar prueba Finalizar función

Análisis de función privada (ByVal theData As String) como entero

Dim Ubicación As.Integer Ubicación = theData.IndexOf(" amazon.com

Rango de ventas:") _

+ "Rango de ventas de Amazon.com:".Longitud

Dim temp como cadena

Hazlo hasta que los Datos.Subcadena(Ubicación.l) = "<" temp = temp

&theData.Substring(Ubicación.l) Ubicación += 1 Bucle

Retorno Clnt(temp)

función final

clase final

Las operaciones de subprocesos múltiples son comunes en los espacios de nombres de .NET y E/S, por lo que la biblioteca de .NET Framework proporciona métodos asincrónicos dedicados para ellas. Para obtener más información sobre el uso de métodos asincrónicos al escribir programas de subprocesos múltiples, consulte los métodos BeginGetResponse y EndGetResponse de la clase HTTPWebRequest.

Peligro principal (datos generales)

Hasta ahora, se ha considerado el único caso de uso seguro para subprocesos: nuestros hilos no cambiaron los datos compartidos. Si permite el cambio de datos compartidos, los errores potenciales comienzan a multiplicarse exponencialmente y se vuelve mucho más difícil que el programa se deshaga de ellos. Por otro lado, si deshabilita la modificación de datos compartidos por diferentes subprocesos, la programación multiproceso .NET prácticamente no será diferente de las características limitadas de VB6.

Te ofrecemos un pequeño programa que demuestra los problemas que se presentan, sin profundizar en detalles innecesarios. Este programa simula una casa con un termostato en cada habitación. Si la temperatura es 5 o más grados Fahrenheit (alrededor de 2,77 grados Celsius) menos de lo normal, le decimos al sistema de calefacción que aumente la temperatura en 5 grados; de lo contrario, la temperatura aumenta solo 1 grado. Si la temperatura actual es mayor o igual a la temperatura configurada, no se realiza ningún cambio. El control de temperatura en cada habitación se lleva a cabo mediante una corriente separada con un retraso de 200 milisegundos. El trabajo principal se realiza mediante el siguiente fragmento:

Si mHouse.HouseTemp< mHouse.MAX_TEMP = 5 Then Try

Subproceso.Sueño(200)

Atrapar empate como ThreadInterruptedException

"La espera pasiva ha sido interrumpida

Atrapar e como excepción

" Otras excepciones Finalizar intento

mHouse.HouseTemp +- 5" Etc.

A continuación se muestra el código fuente completo del programa. El resultado se muestra en la figura. 10.6: ¡La temperatura en la casa alcanzó los 105 grados Fahrenheit (40.5 grados Celsius)!

1 opción estricta

2 Sistema de Importaciones. Roscado

3 Módulo

4 subprincipal()

5 Atenuar miCasa como Casa Nueva(l0)

6 consola. LeerLínea()

7 Finalizar sub

Módulo de 8 extremos

9 Casa de clase pública

10 Const público MAX_TEMP como entero = 75

11 privado mCurTemp como entero = 55

12 mRooms privadas () como habitación

13 Public Sub New(ByVal numOfRooms As Integer)

14 ReDim mRooms(numOfRooms = 1)

15 Dim i como entero

16 Dim aThreadStart como Threading.ThreadStart

17 Atenuar un hilo como hilo

18 For i = 0 To numOfRooms -1

19 Prueba

20 mHabitaciones(i)=NuevaHabitación(Yo, mCurTemp,CStr(i) &"salón")

21 aThreadStart - Nuevo ThreadStart(AddressOf _

mRooms(i).CheckTempInRoom)

22 aSubproceso =Nuevo Subproceso(aSubprocesoInicio)

23 unHilo.Inicio()

24 Captura E como excepción

25 Consola.WriteLine(E.StackTrace)

26 Finalizar intento

27 Siguiente

28 Finalizar Sub

29 Propiedad pública HouseTemp() como entero

treinta . Obtener

31 Devolver mCurTemp

32 Fin Obtener

33 Conjunto (Valor ByVal como entero)

34 mCurTemp = Valor 35 Finalizar ajuste

36 Propiedad final

37 clase final

38 Sala de clase pública

39 Privado mCurTemp como entero

40 mName privado como cadena

41 Casa Privada Como Casa

42 Public Sub New (ByVal theHouse As House,

ByVal temp como entero, ByVal roomName como cadena)

43 mCasa = laCasa

44 mCurTemp = temperatura

45 mNombre = nombre de la habitación

46 Finalizar Sub

47 Suscripción pública CheckTempInRoom()

48 CambiarTemperatura()

49 Finalizar Sub

50 Sub privado ChangeTemperature ()

51 Prueba

52 Si mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

53 Hilo. Dormir (200)

54 mHouse.HouseTemp +- 5

55 Console.WriteLine("Estoy en " & Me.mName & _

56 ". La temperatura actual es "&mHouse.HouseTemp)

57 . Elself mHouse.HouseTemp< mHouse.MAX_TEMP Then

58 hilos. Dormir (200)

59 mCasa.Temp.Casa += 1

60 Console.WriteLine("Estoy en " & Me.mName & _

61 ".La temperatura actual es " & mHouse.HouseTemp)

62 Más

63 Console.WriteLine("Estoy en " & Me.mName & _

64 ".La temperatura actual es " & mHouse.HouseTemp)

65 "No hagas nada, la temperatura es normal

66 Fin si

67 Capturar tae como ThreadInterruptedException

68 "Se ha interrumpido la espera pasiva

69 Captura e como excepción

70" Otras excepciones

71 Finalizar intento

72 Finalizar Sub

73 Clase final

Arroz. 10.6. Problemas de subprocesos múltiples

En el procedimiento Sub Main (líneas 4-7), se crea una "casa" con diez "habitaciones". La clase House establece la temperatura máxima en 75 grados Fahrenheit (alrededor de 24 grados Celsius). Las líneas 13-28 definen un constructor de casas bastante complejo. Las líneas 18-27 son clave para entender el programa. La línea 20 crea otro objeto de habitación, pasando una referencia al objeto de casa al constructor para que el objeto de habitación pueda hacer referencia a él si es necesario. Las líneas 21-23 ejecutan diez subprocesos para ajustar la temperatura en cada habitación. La clase Room se define en las líneas 38-73. Referencia al objeto House coxpaEstablézcalo en la variable mHouse en el constructor de la clase Room (línea 43). El código para verificar y ajustar la temperatura (líneas 50-66) parece simple y natural, pero como pronto verá, ¡esta impresión es engañosa! Tenga en cuenta que este código está envuelto en un bloque Try-Catch porque el programa usa el método Sleep.

Es poco probable que alguien acepte vivir a una temperatura de 105 grados Fahrenheit (40,5 a 24 grados Celsius). ¿Qué sucedió? El problema es con la siguiente línea:

Si mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

Lo que pasa es que el hilo 1 primero comprueba la temperatura, ve que la temperatura es demasiado baja y la sube 5 grados. Desafortunadamente, antes de que aumente la temperatura, el subproceso 1 se interrumpe y el control se transfiere al subproceso 2. El subproceso 2 verifica la misma variable que no ha sido cambiado todavía hilo 1. Por lo tanto, el hilo 2 también se está preparando para aumentar la temperatura en 5 grados, pero no tiene tiempo para hacerlo y también entra en estado de espera. El proceso continúa hasta que se activa el hilo 1 y pasa al siguiente comando: aumentar la temperatura en 5 grados. El aumento se repite cuando se activan los 10 streams, y los vecinos de la casa lo pasarán mal.

Resolución de problemas: sincronización

En el programa anterior, hay una situación en la que el resultado del programa depende del orden en que se ejecutan los hilos. Para deshacerse de él, debe asegurarse de que los comandos como

Si mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then...

son completamente procesados ​​por el subproceso activo antes de que se interrumpa. Esta propiedad se llama vergüenza atómica - un bloque de código debe ser ejecutado por cada subproceso sin interrupción, como una unidad atómica. El programador de subprocesos no puede interrumpir un grupo de instrucciones combinadas en un bloque atómico hasta que se complete. Cada lenguaje de programación multiproceso tiene sus propias formas de garantizar la atomicidad. En VB .NET, la forma más fácil es usar el comando SyncLock, que se llama con una variable de objeto. Realice cambios menores en el procedimiento ChangeTemperature del ejemplo anterior y el programa funcionará bien:

Sub privado ChangeTemperature() SyncLock(mHouse)

tratar

Si mHouse.HouseTemp< mHouse.MAXJTEMP -5 Then

Subproceso.Sueño(200)

mHouse.HouseTemp += 5

Console.WriteLine("Estoy en " & Me.mName & _

".La temperatura actual es " & mHouse.HouseTemp)

ellos mismos

mHouse.HouseTemp< mHouse. MAX_TEMP Then

Thread.Sleep(200) mHouse.HouseTemp += 1

Console.WriteLine("Estoy en " & Me.mName &_ ".La temperatura actual es " & mHouse.HomeTemp) Else

Console.WriteLineC"Estoy en " & Me.mName & _ ".La temperatura actual es " & mHouse.HouseTemp)

"No hagas nada, la temperatura es normal

End If Catch tie As ThreadInterruptedException

" La espera pasiva fue interrumpida por Catch e As Exception

"Otras excepciones

Finalizar intento

EndSyncLock

final sub

El código del bloque SyncLock se ejecuta atómicamente. El acceso a él desde todos los demás subprocesos se cerrará hasta que el primer subproceso libere el bloqueo con el comando End SyncLock. Si un subproceso en un bloque sincronizado entra en un estado de espera pasivo, el bloqueo se mantiene hasta que el subproceso se interrumpe o se reanuda.

El uso adecuado del comando SyncLock garantiza que su programa sea seguro para subprocesos. Desafortunadamente, el abuso de SyncLock afecta negativamente el rendimiento. La sincronización del código en un programa de subprocesos múltiples reduce la velocidad de su trabajo varias veces. Sincronice solo el código más necesario y elimine el bloqueo lo antes posible.

Las clases de colección base no son seguras para subprocesos, pero .NET Framework incluye versiones seguras para subprocesos de la mayoría de las clases de colección. En estas clases, el código de métodos potencialmente peligrosos se incluye en bloques SyncLock. Las versiones seguras para subprocesos de las clases de colección deben usarse en programas multiproceso siempre que la integridad de los datos se vea comprometida.

Queda por mencionar que las variables de condición se implementan fácilmente usando el comando SyncLock. Para hacer esto, solo necesita sincronizar la escritura con una propiedad booleana común de lectura y escritura, como se hace en el siguiente fragmento:

Variable de condición de clase pública

Casillero privado compartido como objeto = nuevo objeto ()

Privado Compartido mOK Como Booleano Compartido

Propiedad TheConditionVariable()As Boolean

Obtener

Volver Aceptar

Fin de obtener

Set (Valor ByVal como booleano) SyncLock (casillero)

mOK= Valor

EndSyncLock

conjunto final

Propiedad final

clase final

Comando SyncLock y clase Monitor

El uso del comando SyncLock involucra algunas sutilezas que no se muestran en los ejemplos simples anteriores. Por lo tanto, la elección del objeto de sincronización juega un papel muy importante. Intente ejecutar el programa anterior con SyncLock(Me) en lugar de SyncLock(mHouse). ¡La temperatura vuelve a subir por encima del umbral!

Recuerde que el comando SyncLock sincroniza de acuerdo con objeto, pasado como un parámetro, no por fragmento de código. El parámetro SyncLock actúa como una puerta para acceder al fragmento sincronizado desde otros subprocesos. El comando SyncLock(Me) en realidad abre varias puertas diferentes, que es exactamente lo que estaba tratando de evitar con la sincronización. Moralidad:

Para proteger los datos compartidos en una aplicación multiproceso, el comando SyncLock debe sincronizarse en un solo objeto.

Dado que la sincronización es específica del objeto, en algunas situaciones es posible bloquear inadvertidamente otros fragmentos. Supongamos que tiene dos métodos sincronizados primero y segundo, ambos métodos sincronizados en el objeto bigLock. Cuando el subproceso 1 ingresa al primer método y toma bigLock, ningún subproceso podrá ingresar al segundo método, ¡porque el acceso ya está restringido al subproceso 1!

La funcionalidad del comando SyncLock se puede considerar como un subconjunto de la funcionalidad de la clase Monitor. La clase Monitor es altamente configurable y puede usarse para resolver tareas de sincronización no triviales. El comando SyncLock es un análogo aproximado de los métodos Enter y Exit de la clase Monitor:

tratar

Monitor.Introducir(elObjeto) Finalmente

Monitor.Salir(elObjeto)

Finalizar intento

Para algunas operaciones estándar (aumento/disminución de una variable, intercambio del contenido de dos variables), .NET Framework proporciona la clase Interlocked, cuyos métodos realizan estas operaciones a nivel atómico. Con la clase Interlocked, estas operaciones son mucho más rápidas que con el comando SyncLock.

Punto muerto

Durante la sincronización, el bloqueo se establece en objetos, no en subprocesos, por lo que al usar diferente objetos para bloquear diferente los fragmentos de código en los programas a veces tienen errores muy no triviales. Desafortunadamente, en muchos casos, la sincronización de un solo objeto es simplemente inaceptable porque hará que los subprocesos se bloqueen con demasiada frecuencia.

Considere la situación entrelazar(interbloqueo) en su forma más simple. Imagine a dos programadores en una mesa para cenar. Desafortunadamente, para dos de ellos solo tienen un cuchillo y un tenedor. Suponiendo que se necesitan tanto un cuchillo como un tenedor para comer, son posibles dos situaciones:

  • Un programador logra agarrar un cuchillo y un tenedor y comienza a comer. Cuando está saciado, deja el juego de la cena y luego otro programador puede tomarlos.
  • Un programador toma el cuchillo y el otro toma el tenedor. Ninguno podrá empezar a comer a menos que el otro le entregue su aparato.

En un programa multiproceso, esta situación se llama bloqueo mutuo. Los dos métodos están sincronizados en diferentes objetos. El subproceso A toma el objeto 1 e ingresa al fragmento de programa protegido por este objeto. Desafortunadamente, para funcionar, necesita acceso al código protegido por otro bloque de bloqueo de sincronización con un objeto de sincronización diferente. Pero antes de que pueda ingresar al fragmento que está siendo sincronizado por otro objeto, el hilo B ingresa y agarra ese objeto. Ahora el subproceso A no puede ingresar al segundo fragmento, el subproceso B no puede ingresar al primer fragmento y ambos subprocesos están condenados a una espera interminable. Ningún subproceso puede continuar ejecutándose porque el objeto necesario para hacerlo nunca se liberará.

Es difícil diagnosticar interbloqueos porque pueden ocurrir en casos relativamente raros. Todo depende del orden en que el planificador les asigne el tiempo de CPU. Es muy posible que, en la mayoría de los casos, los objetos de sincronización se adquieran en un orden sin puntos muertos.

A continuación se muestra una implementación de la situación de punto muerto que se acaba de describir. Después de una breve discusión de los puntos más fundamentales, mostraremos cómo reconocer una situación de interbloqueo en la ventana del subproceso:

1 opción estricta

2 Sistema de Importaciones. Roscado

3 Módulo

4 subprincipal()

5 Dim Tom como nuevo programador ("Tom")

6 Dim Bob como nuevo programador ("Bob")

7 Dim aThreadStart como nuevo ThreadStart(AddressOf Tom.Eat)

8 Dim aThread como nuevo hilo (aThreadStart)

9 unHilo.Nombre="Tom"

10 Dim bThreadStart como nuevo ThreadStarttAddressOf Bob.Eat)

11 Dim bThread como nuevo hilo (bThreadStart)

12 bHilo.Nombre = "Bob"

13 aHilo.Inicio()

14 bHilo.Inicio()

15 Finalizar sub

16 módulo final

17 Tenedor de clase pública

18 Privado Compartido mForkAvaiTable As Boolean = True

19 Propietario privado compartido As String = "Nadie"

20 Propiedad privada de solo lectura OwnsUtensil() como cadena

21 Obtener

22 Regresar segadora

23 Fin Obtener

24 Propiedad final

25 Public Sub GrabForktByVal como programador)

26 Console.Writel_ine(Thread.CurrentThread.Name &_

"tratando de agarrar el tenedor.")

27 Console.WriteLine(Me.OwnsUtensil & "tiene el tenedor") . .

28 Monitor.Ingrese (yo) "SyncLock (un tenedor)"

29 Si mForkDisponible Entonces

30 a.TieneTenedor = Verdadero

31 mOwner = a.MiNombre

32 mTenedorDisponible= Falso

33 Console.WriteLine(a.MyName&"acabo de recibir la bifurcación.esperando")

34 Prueba

Thread.Sleep(100) Captura e como excepción Console.WriteLine(e.StackTrace)

Finalizar intento

35 Fin si

36 Monitor.Salir(Yo)

EndSyncLock

37 Finalizar Sub

38 clase final

39 Cuchillo de clase pública

40 Privado Compartido mKnifeAvailable As Boolean = True

41 Mowner privado compartido As String ="Nadie"

42 Propiedad privada de solo lectura OwnsUtensi1() como cadena

43 Obtener

44 Segadora de retorno

45 Fin Obtener

46 Propiedad final

47 Public Sub GrabKnifetByVal como programador)

48 Console.WriteLine(Thread.CurrentThread.Name & _

"tratando de agarrar el cuchillo.")

49 Console.WriteLine(Me.OwnsUtensil & "tiene el cuchillo")

50 Monitor.Ingrese (yo) "SyncLock (un cuchillo)"

51 Si mKnifeDisponible Entonces

52 mKnifeAvailable = Falso

53 a.HasKnife = Verdadero

54 mOwner = a.MiNombre

55 Console.WriteLine(a.MyName&"acabo de recibir el cuchillo.esperando")

56 Prueba

Subproceso.Sueño(100)

Atrapar e como excepción

Console.WriteLine(e.StackTrace)

Finalizar intento

57 Terminar si

58 Monitor.Salir(Yo)

59 Finalizar Sub

60 clase final

61 Programador de clase pública

62 mName privado como cadena

63 mFork privado compartido como tenedor

64 mKnife privado compartido como cuchillo

65 Privado mHasKnife como booleano

66 Privado mHasFork como booleano

67 Subcompartido Nuevo()

68 mTenedor = Nuevo Tenedor()

69 mCuchillo = Nuevo cuchillo()

70 Finalizar Sub

71 Public Sub New (ByVal theName As String)

72 mNombre = elNombre

73 Finalizar Sub

74 Propiedad pública de solo lectura MyName() como cadena

75 Obtener

76 Devolver mNombre

77 Fin Obtener

78 Propiedad final

79 Propiedad pública HasKnife() como booleano

80 Obtener

81 Regresar mHasKnife

82 Fin Obtener

83 Set (Valor ByVal como booleano)

84 mHasKnife = Valor

85 conjunto final

86 Propiedad final

87 Propiedad pública HasFork() como booleana

88 Obtener

89 Volver mHasFork

90 Fin Obtener

91 Set (Valor ByVal como booleano)

92 mHasTenedor = Valor

93 Juego final

94 Propiedad final

95 Subcomer pública()

96 Hazlo hasta que yo.HasKnife y Yo.HasFork

97 Console.Writeline(Thread.CurrentThread.Name&"está en el hilo").

98 Si Rnd()< 0.5 Then

99 mTenedor.AgarrarTenedor(Me)

100 más

101 mKnife.GrabKnife(Me)

102 Fin si

103 Bucle

104 MsgBox(Me.MyName & "¡puede comer!")

105 mCuchillo = Nuevo cuchillo()

106 mTenedor= Nuevo Tenedor()

107 Finalizar Sub

108 clase final

El procedimiento principal Main (líneas 4-16) crea dos instancias de la clase Programmer y luego inicia dos subprocesos para ejecutar el método Eat crítico de la clase Programmer (líneas 95-108), que se describe a continuación. El procedimiento Main establece los nombres de los subprocesos y los inicia; probablemente, todo lo que sucede es claro y sin comentarios.

El código de la clase Fork parece más interesante (líneas 17-38) (una clase Knife similar se define en las líneas 39-60). Las líneas 18 y 19 establecen los valores de los campos comunes, mediante los cuales puede averiguar si la bifurcación está disponible actualmente y, de no ser así, quién la usa. La propiedad de solo lectura OwnUtensi1 (líneas 20 a 24) está diseñada para la transferencia de información más simple. El elemento central de la clase Fork es el método "agarrar el tenedor" de GrabFork, definido en las líneas 25-27.

  1. Las líneas 26 y 27 simplemente escriben información de depuración en la consola. En el código principal del método (líneas 28-36), el acceso a la bifurcación está sincronizado en la ruta del objeto.cinturón Yo. Debido a que nuestro programa usa solo una bifurcación, el tiempo en mí asegura que dos subprocesos no puedan agarrarlo al mismo tiempo. El comando Sleep "p (en el bloque que comienza en la línea 34) simula el retraso entre agarrar el tenedor/cuchillo y el comienzo de la comida. ¡Tenga en cuenta que el comando Sleep no libera el bloqueo de los objetos y solo acelera el interbloqueo!
    Sin embargo, el código de la clase Programador (líneas 61-108) es de mayor interés. Las líneas 67-70 definen un constructor genérico para garantizar que solo haya un tenedor y un cuchillo en el programa. El código de propiedad (líneas 74-94) es simple y no requiere comentarios. Lo más importante sucede en el método Eat, que es ejecutado por dos subprocesos separados. El proceso continúa en bucle hasta que algún hilo captura el tenedor junto con el cuchillo. En las líneas 98-102, el objeto agarra un tenedor/cuchillo al azar usando la llamada Rnd, que es lo que causa el interbloqueo. Sucede lo siguiente:
    El subproceso que ejecuta el método Eat del objeto One se activa y entra en el bucle. Agarra el cuchillo y entra en un estado de espera.
  2. El subproceso que ejecuta el método Eat de Bob se activa y entra en el bucle. No puede agarrar el cuchillo, pero sí el tenedor y entra en estado de espera.
  3. El subproceso que ejecuta el método Eat del objeto One se activa y entra en el bucle. Intenta agarrar el tenedor, pero Bob ya lo ha agarrado; el hilo entra en el estado de espera.
  4. El subproceso que ejecuta el método Eat de Bob se activa y entra en el bucle. Intenta agarrar el cuchillo, pero Thoth ya lo agarró; el hilo entra en el estado de espera.

Todo esto continúa hasta el infinito: tenemos una situación típica de punto muerto (intente ejecutar el programa y verá que nadie logra comer así).
También puede obtener información sobre la aparición de un interbloqueo en la ventana de subprocesos. Ejecuta el programa e interrúmpelo con las teclas Ctrl+Break. Incluya la variable Yo en la ventana gráfica y abra la ventana de subprocesos. El resultado se parece al que se muestra en la Fig. 10.7. De la figura, se puede ver que el hilo de Bob ha agarrado un cuchillo, pero no tiene un tenedor. Haga clic con el botón derecho en la ventana de hilos en la línea Toth y seleccione el comando Cambiar a hilo en el menú contextual. La ventana muestra que el arroyo Thoth tiene un tenedor pero no un cuchillo. Por supuesto, esto no es una prueba al cien por cien, pero tal comportamiento al menos hace sospechar que algo andaba mal.
Si la opción de sincronizar en un solo objeto (como en el programa con un aumento de la temperatura en la casa) no es posible, para evitar interbloqueos, puede numerar los objetos de sincronización y capturarlos siempre en un orden constante. Para continuar con la analogía con los programadores cenando: si el hilo siempre toma primero el cuchillo y luego el tenedor, no habrá problemas de interbloqueo. El primer hilo que agarre el cuchillo podrá comer normalmente. Traducido al lenguaje de los flujos de programa, esto significa que la captura del objeto 2 solo es posible si el objeto 1 es capturado previamente.

Arroz. 10.7. Análisis de punto muerto en la ventana Subprocesos

Por lo tanto, si eliminamos la llamada a Rnd en la línea 98 y la reemplazamos con el fragmento

mTenedor.AgarrarTenedor(Me)

mKnife.GrabKnife(Me)

¡desaparece el punto muerto!

Colabore en los datos a medida que se crean

Una situación común en las aplicaciones de subprocesos múltiples es cuando los subprocesos no solo funcionan con datos compartidos, sino que también esperan a que lleguen (es decir, el subproceso 1 debe crear los datos antes de que el subproceso 2 pueda usarlos). Dado que los datos son compartidos, el acceso a ellos debe estar sincronizado. También es necesario proporcionar medios para notificar a los subprocesos en espera cuando los datos estén listos.

Tal situación suele llamarse problema proveedor/consumidor. Un subproceso intenta acceder a datos que aún no existen, por lo que debe transferir el control a otro subproceso que crea los datos necesarios. El problema se resuelve con el siguiente código:

  • El subproceso 1 (el consumidor) se despierta, ingresa al método sincronizado, busca datos, no los encuentra y entra en estado de espera. PreliminarSin embargo, debe liberar el bloqueo para no interferir con el trabajo del subproceso del proveedor.
  • El subproceso 2 (el proveedor) ingresa al método sincronizado liberado por el subproceso 1, crea data para el subproceso 1 y de alguna manera notifica al subproceso 1 de la presencia de datos. Luego libera el bloqueo para que Thread 1 pueda procesar los nuevos datos.

No intente resolver este problema activando constantemente el subproceso 1 mientras verifica el estado de una variable de condición cuyo valor es >establecido por el subproceso 2. Esta solución afectará seriamente el rendimiento de su programa, porque en la mayoría de los casos el subproceso 1 se activará. sin razón; y el subproceso 2 esperará con tanta frecuencia que no tendrá tiempo para crear datos.

Las relaciones productor/consumidor son muy comunes, por lo que las bibliotecas de clases de programación de subprocesos múltiples crean primitivos especiales para tales situaciones. En .NET, estas primitivas se denominan Wait y Pulse-PulseAl 1 y forman parte de la clase Monitor. La figura 10.8 explica la situación que estamos a punto de programar. Hay tres colas de subprocesos en el programa: la cola de espera, la cola de bloqueo y la cola de ejecución. El programador de subprocesos no asigna tiempo de CPU a los subprocesos que están en la cola de espera. Para que se le asigne tiempo a un subproceso, debe pasar a la cola de ejecución. Como resultado, el trabajo de la aplicación se organiza de manera mucho más eficiente que con el sondeo habitual de una variable condicional.

En pseudocódigo, el idioma del consumidor de datos se formula de la siguiente manera:

"Ingresando un bloque sincronizado de la siguiente forma

mientras no hay datos

Ir a la cola de espera

círculo

Si hay datos, procéselos.

Salir del bloque sincronizado

Inmediatamente después de ejecutar el comando Wait, el subproceso se suspende, se libera el bloqueo y el subproceso ingresa a la cola de espera. Cuando se libera el bloqueo, el subproceso en la cola de ejecución puede ejecutarse. Con el tiempo, uno o más subprocesos bloqueados crearán los datos necesarios para el trabajo del subproceso en la cola de espera. Dado que la validación de datos se realiza en un ciclo, la transición al uso de datos (después del ciclo) ocurre solo cuando hay datos listos para procesar.

En pseudocódigo, el idioma del proveedor de datos se ve así:

"Ingresar a un bloque de vista sincronizada

Si bien NO se necesitan datos

Ir a la cola de espera

De lo contrario, producir datos

Una vez que los datos estén listos, llame a Pulse-PulseAll.

para mover uno o más subprocesos de la cola de bloqueo a la cola de ejecución. Dejar el bloque sincronizado (y volver a la cola de ejecución)

Supongamos que nuestro programa está simulando una familia con un padre que gana dinero y un hijo que gasta ese dinero. Cuando el dinero se acabeutsya, el niño tiene que esperar la llegada de una nueva cantidad. La implementación de software de este modelo se ve así:

1 opción estricta

2 Sistema de Importaciones. Roscado

3 Módulo

4 subprincipal()

5 Atenuar la familia como nueva familia ()

6 laFamilia.StartltsLife()

7 Finalizar sub

8 Fjodulo final

9

10 Familia de clase pública

11 dinero privado como entero

12 mWeek privado como entero = 1

13 Public Sub StartltsLife()

14 Dim aThreadStart como nuevo ThreadStarUAddressOf Me.Produce)

15 Dim bThreadStart como nuevo ThreadStarUAddressOf Me.Consume)

16 Dim aThread como nuevo hilo (aThreadStart)

17 Dim bThread como nuevo hilo (bThreadStart)

18 unHilo.Nombre = "Producir"

19 aHilo.Inicio()

20 bHilo.Nombre = "Consumir"

21b hilo. Comienzo()

22 Finalizar Sub

23 Propiedad pública TheWeek() como entero

24 Obtener

25 Retorno msemana

26 Fin Obtener

27 Conjunto (Valor ByVal como entero)

28 msemana - Valor

29 conjunto final

30 Propiedad final

31 Propiedad pública OurMoney() como entero

32 Obtener

33 Devolución de dinero

34 Fin Obtener

35 Conjunto (Valor ByVal como entero)

36 millonesDinero =Valor

37 conjunto final

38 Propiedad final

39 Public Sub Produce ()

40 hilos. Dormir (500)

41 hacer

42 Monitor.Entrar(Yo)

43 Haz Mientras Yo.NuestroDinero > 0

44 Monitorear.Esperar(Yo)

45 bucle

46 Yo.NuestroDinero=1000

47 Monitor.PulseAll(Yo)

48 Monitor.Salir(Yo)

49 Bucle

50 Finalizar sub

51 Subconsumo público()

52 MsgBox("Estoy en hilo de consumo")

53 hacer

54 Monitor.Entrar(Yo)

55 Hacer Mientras Yo.NuestroDinero = 0

56 Monitorear.Esperar(Yo)

57 Bucle

58 Console.WriteLine("Querido padre, acabo de gastar todo tu " & _

dinero en la semana" y la semana)

59 La semana += 1

60 Si La Semana = 21 *52 Entonces Sistema.Entorno.Salir(0)

61 Yo.NuestroDinero=0

62 Monitor.PulseAll(Yo)

63 Monitor.Salir(Yo)

64 Bucle

65 Finalizar Sub

66 Clase final

El método StartltsLife (líneas 13-22) se prepara para iniciar los subprocesos Produce y Consume. Lo más importante sucede en los flujos Produce (líneas 39-50) y Consume (líneas 51-65). El procedimiento Sub Produce verifica la disponibilidad de dinero, y si hay dinero, pasa a la cola de espera. De lo contrario, el padre genera dinero (línea 46) y notifica a los objetos en la cola de espera sobre el cambio de situación. Tenga en cuenta que la llamada Pulse-Pulse All solo tiene efecto cuando se libera el bloqueo con el comando Monitor.Exit. Por el contrario, el procedimiento Sub Consume verifica la presencia de dinero y, si no hay dinero, notifica al padre que espera. La línea 60 simplemente termina el programa después de 21 años condicionales; Sistema de llamadas. Environment.Exit(0) es la contraparte de .NET del comando End (el comando End también es compatible, pero a diferencia de System.Environment.Exit, no devuelve un código de salida al sistema operativo).

Los subprocesos colocados en la cola de espera deben ser liberados por otras partes de su programa. Esta es la razón por la que preferimos usar PulseAll en lugar de Pulse. Dado que no se sabe de antemano qué subproceso se activará cuando se llame a Pulse 1, con un número relativamente pequeño de subprocesos en la cola, se puede llamar a PulseAll con el mismo éxito.

Multihilo en programas gráficos

Nuestra discusión sobre subprocesos múltiples en aplicaciones GUI comenzará con un ejemplo que explica para qué sirve el subproceso múltiple en aplicaciones GUI. Cree un formulario con dos botones Iniciar (btnStart) y Cancelar (btnCancel), como se muestra en la Figura 1. 10.9. Cuando se hace clic en el botón Inicio, se crea una clase que contiene una cadena aleatoria de 10 millones de caracteres y un método para contar las apariciones de la letra "E" en esa larga cadena. Tenga en cuenta el uso de la clase StringBuilder, que hace que la construcción de cadenas largas sea más eficiente.

Paso 1

El subproceso 1 advierte que no hay datos para él. Llama a Wait, libera el bloqueo y entra en la cola de espera.



Paso 2

Cuando se libera el bloqueo, el subproceso 2 o el subproceso 3 sale de la cola de bloqueo y entra en el bloque sincronizado, adquiriendo el bloqueo.

Paso 3

Digamos que el subproceso 3 ingresa a un bloque sincronizado, crea datos y llama a Pulse-Pulse All.

Inmediatamente después de salir del bloque y liberar el bloqueo, el subproceso 1 se mueve a la cola de ejecución. Si el subproceso 3 llama a Pluse, solo uno va a la cola de ejecución.subproceso, cuando se llama a Pluse All, todos los subprocesos van a la cola de ejecución.



Arroz. 10.8. El problema proveedor/consumidor

Arroz. 10.9. Subprocesamiento múltiple en una aplicación GUI simple

Importaciones System.Text

Personajes aleatorios de clase pública

m_Data privado como StringBuilder

mjength privado, m_count como entero

Public Sub New (ByVal n As Integer)

m_Longitud = n-1

m_Data = Nuevo StringBuilder(m_length) MakeString()

final sub

Sub privado MakeString()

Dim i como entero

Dim myRnd como nuevo aleatorio ()

Para i = 0 Para m_longitud

" Generar un número aleatorio del 65 al 90,

"conviértelo a mayúsculas

" y adjunte al objeto StringBuilder

m_Data.Append(Chr(myRnd.Next(65.90)))

próximo

final sub

Public Sub StartCount ()

ObtenerEes()

final sub

Suscripción privada GetEes()

Dim i como entero

Para i = 0 Para m_longitud

Si m_Data.Chars(i) = CCar("E") Entonces

m_cuenta += 1

Terminar si sigue

m_CountDone = Verdadero

final sub

Solo lectura pública

Propiedad GetCount() como entero Obtener

Si no (m_CountDone) Entonces

Devolver m_count

Terminara si

Fin Obtener propiedad final

Solo lectura pública

Propiedad IsDone() como booleano Obtener

regreso

m_CountDone

Fin de obtener

Propiedad final

clase final

Los dos botones del formulario tienen un código muy simple asociado a ellos. El procedimiento btn-Start_Click instancia la clase RandomCharacters anterior, que encapsula una cadena con 10 millones de caracteres:

Sub privado btnStart_Click (ByVal remitente As System.Object.

ByVal y As System.EventArgs) Maneja btnSTart.Click

Dim RC como nuevos caracteres aleatorios (10000000)

RC.StartCount()

MsgBox("El número de es es " & RC.GetCount)

final sub

El botón Cancelar muestra un cuadro de mensaje:

Sub privado btnCancel_Click (ByVal remitente As System.Object._

ByVal e As System.EventArgs)Maneja btnCancel.Click

MsgBox("Cuenta interrumpida!")

final sub

Cuando ejecuta el programa y hace clic en el botón Inicio, descubre que el botón Cancelar no responde a la entrada del usuario porque el bucle continuo impide que el botón controle el evento que recibe. ¡En los programas modernos, esto es inaceptable!

Dos soluciones son posibles. La primera opción, bien conocida por versiones anteriores de VB, prescinde de subprocesos múltiples: la llamada DoEvents se incluye en el bucle. En .NET, este comando se ve así:

Aplicación.DoEvents()

En nuestro ejemplo, esto es definitivamente indeseable: ¡quién quiere ralentizar el programa con diez millones de llamadas DoEvents! Si, en cambio, separa el bucle en un subproceso separado, el sistema operativo cambiará entre subprocesos y el botón Cancelar seguirá funcionando. A continuación se muestra una implementación con un subproceso separado. Para mostrar visualmente que el botón Cancelar funciona, cuando se presiona, simplemente finalizamos el programa.

Siguiente paso: Mostrar botón de conteo

Digamos que decide ser creativo y darle a la forma el aspecto que se muestra en la fig. 10.9. Tenga en cuenta: el botón Mostrar recuento aún no está disponible.

Arroz. 10.10. Formulario con botón deshabilitado

Se supone que un hilo separado hace el conteo y desbloquea el botón deshabilitado. Por supuesto que se puede hacer; además, tal problema surge con bastante frecuencia. Desafortunadamente, no podrá hacerlo de la manera más obvia: vincular un subproceso secundario a un subproceso GUI manteniendo una referencia al botón ShowCount en el constructor, o incluso usando un delegado estándar. En otras palabras, nunca no use la opción a continuación (básico erróneo las líneas están en negrita).

Personajes aleatorios de clase pública

m_0ata privado como StringBuilder

Privado m_CountDone como booleano

Longitud privada. m_count como entero

Botón m_privado como Windows.Forms.Button

Public Sub New(ByVa1 n As Integer,_

ByVal b AsWindows.Forms.Button)

m_longitud = n - 1

m_Data = Nuevo StringBuilder (mJength)

m_Button = b CrearCadena()

final sub

Sub privado MakeString()

Dim I como entero

Dim myRnd como nuevo aleatorio ()

Para I = 0 Para m_longitud

m_Data.Append(Chr(myRnd.Next(65.90)))

próximo

final sub

Public Sub StartCount ()

ObtenerEes()

final sub

Suscripción privada GetEes()

Dim I como entero

Para I = 0 Para mjength

Si m_Data.Chars(I) = CCar("E") Entonces

m_cuenta += 1

Terminar si sigue

m_CountDone =Verdadero

m_Button.Enabled=Verdadero

final sub

Solo lectura pública

Propiedad GetCount()As Integer

Obtener

Si no (m_CountDone) Entonces

Lanzar nueva excepción ("Conteo aún no realizado") Else

Devolver m_count

Terminara si

Fin de obtener

Propiedad final

Propiedad pública de solo lectura IsDone() como booleana

Obtener

Devolver m_CountDone

Fin de obtener

Propiedad final

clase final

Es probable que en algunos casos este código funcione. Sin embargo:

  • La interacción del subproceso secundario con el subproceso que crea la GUI no se puede organizar obvio medio.
  • Nunca no cambie los elementos en los programas de gráficos de otros subprocesos del programa. Todos los cambios solo deben ocurrir en el subproceso que creó la GUI.

Si rompe estas reglas, nosotros nosotros garantizamos que sutiles, sutiles errores ocurrirán en sus programas de gráficos de subprocesos múltiples.

Tampoco será posible organizar la interacción de los objetos mediante eventos. El trabajador de 06 eventos se ejecuta en el mismo subproceso en el que se llamó a RaiseEvent, por lo que los eventos no lo ayudarán.

Sin embargo, el sentido común dicta que las aplicaciones gráficas deben tener un medio para modificar elementos de otro subproceso. .NET Framework proporciona una forma segura para subprocesos de llamar a métodos de aplicación GUI desde otro subproceso. Para este propósito, se utiliza un tipo de delegado de Method Invoker especial del espacio de nombres System.Windows. formularios El siguiente fragmento muestra la nueva versión del método GetEes (las líneas modificadas están en negrita):

Suscripción privada GetEes()

Dim I como entero

Para I = 0 Para m_longitud

Si m_Data.Chars(I) = CCar("E")Entonces

m_cuenta += 1

Terminar si sigue

m_CountDone = Prueba verdadera

Dim myInvoker como nuevo MethodInvoker (AddressOf UpDateButton)

myInvoker.Invoke() Captura e como ThreadInterruptedException

"Falla

Finalizar intento

final sub

Botón de actualización de suscripción pública ()

m_Button.Enabled =Verdadero

final sub

Las llamadas entre subprocesos al botón no se realizan directamente, sino a través de Method Invoker. .NET Framework garantiza que esta opción es segura para subprocesos.

¿Por qué la programación de subprocesos múltiples causa tantos problemas?

Ahora que tiene una idea de la programación de subprocesos múltiples y los problemas potenciales asociados con ella, pensamos que sería apropiado responder la pregunta planteada en el título de la subsección al final de este capítulo.

Una de las razones es que los subprocesos múltiples son un proceso no lineal y estamos acostumbrados a un modelo de programación lineal. Al principio, es difícil acostumbrarse a la idea de que la ejecución del programa puede interrumpirse aleatoriamente y el control se transferirá a otro código.

Sin embargo, hay otra razón más fundamental: muy pocos programadores en estos días programan en ensamblador, o incluso miran la salida desensamblada de un compilador. De lo contrario, les sería mucho más fácil acostumbrarse a la idea de que decenas de instrucciones del ensamblador pueden corresponder a un comando de un lenguaje de alto nivel (como VB .NET). El hilo se puede interrumpir después de cualquiera de estas instrucciones y, por lo tanto, también en medio de una instrucción de alto nivel.

Pero eso no es todo: los compiladores modernos optimizan la velocidad de los programas y el hardware de la computadora puede interferir con la administración de la memoria. Como resultado, el compilador o el hardware pueden, sin su conocimiento, cambiar el orden de los comandos especificados en el código fuente del programa [ Muchos compiladores optimizan las operaciones de copia de matrices circulares como i=0 a n:b(i)=a(i):ncxt. ¡El compilador (o incluso un administrador de memoria especializado) puede simplemente crear una matriz y luego llenarla con una sola operación de copia en lugar de copiar elementos individuales varias veces!].

Esperamos que estas explicaciones lo ayuden a comprender mejor por qué la programación multiproceso causa tantos problemas, ¡o al menos se sorprenda menos por el extraño comportamiento de sus programas multiproceso!

Publicaciones anteriores cubrieron subprocesos múltiples en Windows usando CreateThread y otras WinAPI, y subprocesos múltiples en Linux y otros sistemas *nix usando pthreads. Si está escribiendo en C++ 11 o posterior, tiene acceso a std::thread y otras primitivas de subprocesos múltiples introducidas en este estándar de lenguaje. A continuación se muestra cómo trabajar con ellos. A diferencia de WinAPI y pthreads, el código escrito con std::thread es multiplataforma.

Nota: El código anterior se probó en GCC 7.1 y Clang 4.0 en Arch Linux, GCC 5.4 y Clang 3.8 en Ubuntu 16.04 LTS, GCC 5.4 y Clang 3.8 en FreeBSD 11 y Visual Studio Community 2017 en Windows 10. CMake anterior a la versión 3.8 no puede Speak compilador para usar el estándar C++17 especificado en las propiedades del proyecto. Cómo instalar CMake 3.8 en Ubuntu 16.04. Para que el código se compile con Clang, el paquete libc++ debe estar instalado en los sistemas *nix. Para Arch Linux, el paquete está disponible en AUR. Ubuntu tiene el paquete libc++-dev, pero puede encontrarse con , lo que hace que el código no se compile tan fácilmente. La solución alternativa se describe en StackOverflow. En FreeBSD, debe instalar el paquete cmake-modules para compilar el proyecto.

mutexes

A continuación se muestra un ejemplo simple del uso de subprocesos y mutexes:

#incluir
#incluir
#incluir
#incluir

estándar::mutex mtx;
contador int estático = 0;


por (;; ) (
{
std::lock_guard< std:: mutex >bloquear(mtx) ;

descanso ;
int ctr_val = ++ contador;
estándar::fuera<< "Thread " << tnum << ": counter = " <<
valor_ctr<< std:: endl ;
}

}
}

int principal() (
estándar::vector< std:: thread >hilos;
para (int i = 0 ; i< 10 ; i++ ) {


}

// no se puede usar const auto& aquí ya que .join() no está marcado como const

thr.join();
}

estándar::cout<< "Done!" << std:: endl ;
devolver 0;
}

Tenga en cuenta el ajuste de std::mutex en std::lock_guard, siguiendo el lenguaje RAII. Este enfoque garantiza que la exclusión mutua se liberará al salir del ámbito en cualquier caso, incluso cuando se produzcan excepciones. Para capturar varios mutex a la vez para evitar interbloqueos, existe una clase std::scoped_lock . Sin embargo, solo apareció en C ++ 17 y, por lo tanto, es posible que no funcione en todas partes. Para versiones anteriores de C++, hay una plantilla std::lock que tiene una funcionalidad similar, aunque requiere código adicional para liberar correctamente los bloqueos a través de RAII.

R.W.Lock

A menudo hay una situación en la que el acceso a un objeto ocurre más a menudo para leer que para escribir. En este caso, en lugar del mutex habitual, es más eficiente usar un bloqueo de lectura y escritura, también conocido como RWLock. RWLock puede ser capturado por varios subprocesos de lectura a la vez o por un solo subproceso de escritura. RWLock en C++ corresponde a las clases std::shared_mutex y std::shared_timed_mutex:

#incluir
#incluir
#incluir
#incluir

// estándar::shared_mutex mtx; // no funcionará con GCC 5.4
estándar::shared_timed_mutex mtx;

contador int estático = 0;
constante estática int MAX_COUNTER_VAL = 100;

void thread_proc(int tnum) (
por (;; ) (
{
// ver también std::shared_lock
estándar::bloqueo_único< std:: shared_timed_mutex >bloquear(mtx) ;
si (contador == MAX_COUNTER_VAL)
descanso ;
int ctr_val = ++ contador;
estándar::fuera<< "Thread " << tnum << ": counter = " <<
valor_ctr<< std:: endl ;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}

int principal() (
estándar::vector< std:: thread >hilos;
para (int i = 0 ; i< 10 ; i++ ) {
std::thread thr(thread_proc, i) ;
threads.emplace_back(std::move(thr) ) ;
}

para (auto & thr: subprocesos) (
thr.join();
}

estándar::cout<< "Done!" << std:: endl ;
devolver 0;
}

Por analogía con std::lock_guard, las clases std::unique_lock y std::shared_lock se utilizan para capturar RWLock, dependiendo de cómo queramos capturar el bloqueo. La clase std::shared_timed_mutex se introdujo en C++14 y funciona en todas* las plataformas modernas (no hablaré por dispositivos móviles, consolas de juegos, etc.). A diferencia de std::shared_mutex, tiene métodos try_lock_for, try_lock_unti y otros que intentan adquirir un mutex dentro de un tiempo determinado. Sospecho firmemente que std::shared_mutex debería ser más barato que std::shared_timed_mutex. Sin embargo, std::shared_mutex solo apareció en C++17, lo que significa que no se admite en todas partes. En particular, el GCC 5.4 todavía ampliamente utilizado no lo sabe.

Almacenamiento local de subprocesos

A veces es necesario crear una variable, como una global, pero solo un subproceso puede verla. Otros subprocesos también ven la variable, pero tienen su propio valor local. Para hacer esto, crearon Thread Local Storage o TLS (¡no tiene nada que ver con la seguridad de la capa de transporte!). Entre otras cosas, TLS se puede utilizar para acelerar significativamente la generación de números pseudoaleatorios. Un ejemplo del uso de TLS en C++:

#incluir
#incluir
#incluir
#incluir

estándar::mutex io_mtx;
hilo_local int contador = 0;
constante estática int MAX_COUNTER_VAL = 10;

void thread_proc(int tnum) (
por (;; ) (
contador++;
si (contador == MAX_COUNTER_VAL)
descanso ;
{
std::lock_guard< std:: mutex >bloquear(io_mtx) ;
estándar::fuera<< "Thread " << tnum << ": counter = " <<
encimera<< std:: endl ;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}

int principal() (
estándar::vector< std:: thread >hilos;
para (int i = 0 ; i< 10 ; i++ ) {
std::thread thr(thread_proc, i) ;
threads.emplace_back(std::move(thr) ) ;
}

para (auto & thr: subprocesos) (
thr.join();
}

estándar::cout<< "Done!" << std:: endl ;
devolver 0;
}

El mutex aquí se usa únicamente para sincronizar la salida a la consola. No se requiere sincronización para acceder a las variables thread_local.

Variables atómicas

Las variables atómicas a menudo se usan para realizar operaciones simples sin el uso de mutexes. Por ejemplo, necesita incrementar un contador de varios subprocesos. En lugar de envolver un int en un std::mutex, es más eficiente usar std::atomic_int. C++ también ofrece std::atomic_char, std::atomic_bool y muchos más tipos. También implementan algoritmos sin bloqueo y estructuras de datos en variables atómicas. Vale la pena señalar que son muy difíciles de desarrollar y depurar, y no todos los sistemas funcionan más rápido que algoritmos y estructuras de datos similares con bloqueos.

Ejemplo de código:

#incluir
#incluir
#incluir
#incluir
#incluir

estándar estático:: atomic_int atomic_counter(0);
constante estática int MAX_COUNTER_VAL = 100;

estándar::mutex io_mtx;

void thread_proc(int tnum) (
por (;; ) (
{
int ctr_val = ++ contador_atomico;
si (ctr_val >= MAX_COUNTER_VAL)
descanso ;

{
std::lock_guard< std:: mutex >bloquear(io_mtx) ;
estándar::fuera<< "Thread " << tnum << ": counter = " <<
valor_ctr<< std:: endl ;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}

int principal() (
estándar::vector< std:: thread >hilos;

int nthreads = std::thread::hardware_concurrency();
if (n hilos == 0 ) n hilos = 2 ;

para (int i = 0 ; i< nthreads; i++ ) {
std::thread thr(thread_proc, i) ;
threads.emplace_back(std::move(thr) ) ;
}

para (auto & thr: subprocesos) (
thr.join();
}

estándar::cout<< "Done!" << std:: endl ;
devolver 0;
}

Tenga en cuenta el uso del procedimiento hardware_concurrency. Devuelve una estimación del número de subprocesos que se pueden ejecutar en paralelo en el sistema actual. Por ejemplo, en una máquina con un procesador de cuatro núcleos que admita hiperprocesamiento, el procedimiento devuelve el número 8. El procedimiento también puede devolver cero si la evaluación falla o simplemente no se implementa.

Para obtener información sobre cómo funcionan las variables atómicas en el nivel del ensamblador, consulte la hoja de referencia de instrucciones básicas del ensamblador x86/x64.

Conclusión

Por lo que puedo ver, todo funciona muy bien. Es decir, al escribir aplicaciones multiplataforma en C ++, puede olvidarse de forma segura de WinAPI y pthreads. Pure C también tiene subprocesos multiplataforma desde C11. Pero todavía no son compatibles con Visual Studio (lo verifiqué) y es poco probable que alguna vez lo sean. No es ningún secreto que Microsoft no ve interés en desarrollar soporte para el lenguaje C en su compilador, prefiriendo concentrarse en C++.

Todavía quedan muchas primitivas detrás de escena: estándar::condición_variable(_cualquiera), std::(shared_)future, std::promise, std::sync y otros. Para familiarizarse con ellos, recomiendo el sitio cppreference.com. También puede tener sentido leer el libro C++ Concurrency in Action. Pero debo advertirte que ya no es nuevo, contiene mucha agua y, en esencia, vuelve a contar una docena de artículos de cppreference.com.

La versión completa del código fuente de esta nota, como siempre, está en GitHub. ¿Cómo se escriben aplicaciones de subprocesos múltiples en C++ ahora?