Menú
Está libre
registro
hogar  /  Multimedia/ Programas multiproceso con ejemplos. Ocho reglas simples para desarrollar aplicaciones multiproceso

Programas multiproceso con ejemplos. Ocho reglas simples para desarrollar aplicaciones multiproceso

La programación multiproceso no es fundamentalmente diferente de escribir interfaces gráficas de usuario basadas en eventos, o incluso escribir aplicaciones secuenciales simples. Aquí se aplican todas las reglas importantes que rigen la encapsulación, la separación de preocupaciones, el acoplamiento flexible, etc. Pero a muchos desarrolladores les resulta difícil escribir programas multiproceso precisamente porque descuidan estas reglas. En su lugar, intentan poner en práctica el conocimiento mucho menos importante sobre subprocesos y primitivas de sincronización, obtenido de los textos sobre programación de subprocesos múltiples para principiantes.

Entonces, cuales son estas reglas

Otro programador, enfrentado a un problema, piensa: "Oh, exactamente, necesitamos aplicar expresiones regulares". Y ahora ya tiene dos problemas: Jamie Zawinski.

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

Demasiados programadores que se comprometen a escribir código multiproceso caen en la trampa, como el héroe de la balada de Goethe " El aprendiz de brujo". El programador aprenderá a crear un montón de hilos que, en principio, funcionan, pero tarde o temprano se salen de control y el programador no sabe qué hacer.

Pero a diferencia de un mago abandonado, el desafortunado programador no puede esperar la llegada de un poderoso hechicero que agitará su varita y restablecerá el orden. En cambio, el programador recurre a los trucos más desagradables, tratando de hacer frente a los problemas que surgen constantemente. El resultado es siempre el mismo: se obtiene una aplicación demasiado complicada, limitada, frágil y poco fiable. Tiene una amenaza persistente de interbloqueo y otros peligros inherentes a un código multiproceso incorrecto. Ni siquiera estoy hablando de fallas inexplicables, bajo rendimiento, resultados de trabajo incompletos o incorrectos.

Es posible que se haya preguntado: ¿por qué sucede esto? Un error común es: "La programación de subprocesos múltiples es muy difícil". Pero este no es el caso. Si un programa de subprocesos múltiples no es confiable, generalmente cambia por las mismas razones que un programa de subproceso único de baja calidad. Es solo que el programador no sigue los métodos de desarrollo fundamentales, bien conocidos y probados. Los programas multiproceso solo parecen más complejos, porque cuanto más subprocesos paralelos salen mal, más desorden hacen, y mucho más rápido de lo que lo haría un solo subproceso.

La idea errónea sobre "la complejidad de la programación de subprocesos múltiples" se ha generalizado debido a que aquellos desarrolladores que se han desarrollado profesionalmente escribiendo código de un solo subproceso, se encontraron por primera vez con subprocesos múltiples y no lo hicieron. Pero en lugar de repensar sus prejuicios y hábitos de trabajo, obstinadamente arreglan el hecho de que no quieren trabajar de ninguna manera. Poniendo excusas para software poco confiable y fechas límite incumplidas, estas personas repiten lo mismo: "la programación multiproceso es muy difícil".

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

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

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

Los conceptos de programación más importantes son universales. Se aplican igualmente a programas de un solo subproceso y de varios subprocesos. Los programadores que se ahogan en una vorágine de flujos simplemente no aprendieron lecciones importantes cuando dominaron el código de un solo subproceso. Puedo decir esto porque dichos desarrolladores cometen los mismos errores fundamentales en programas de un solo subproceso y de varios subprocesos.

Quizás la lección más importante que se puede aprender en sesenta años de historia de la programación es: estado mutable global- maldad... Real maldad. Los programas que se basan en un estado globalmente mutable son relativamente difíciles de razonar y, por lo general, no son fiables porque hay demasiadas formas de cambiar de estado. Ha habido muchos estudios que confirman este principio general, existen innumerables patrones de diseño, cuyo objetivo principal es implementar uno u otro método de ocultación de 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 al número de componentes que pueden alterar los datos.

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

Los problemas del estado globalmente mutable se hicieron evidentes gradualmente a finales de los 80 y principios de los 90, con la proliferación de la programación impulsada por eventos. Los programas ya no se iniciaban "desde el principio" ni seguían una ruta única y predecible de ejecución "hasta el final". Los programas modernos tienen un estado inicial, después de salir de qué eventos ocurren en ellos, en un orden impredecible, con intervalos de tiempo variables. El código sigue siendo de un solo subproceso, pero se vuelve asincrónico. La probabilidad de corrupción de datos aumenta precisamente porque el orden de ocurrencia de los eventos es muy importante. Las situaciones de este tipo son bastante comunes: 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 para intervenir entre ellos, entonces los datos pueden distorsionarse más allá del reconocimiento.

Si se trata de corrientes paralelas, el problema se agrava aún más, ya que varios métodos pueden operar simultáneamente en el estado global. Resulta imposible juzgar cómo cambia exactamente el estado global. Ya estamos hablando no solo del hecho de que los eventos pueden ocurrir en un orden impredecible, sino también del hecho de que se puede actualizar el estado de varios hilos de ejecución. simultaneamente... Con la programación asincrónica, puede, como mínimo, asegurarse de que un evento en particular no pueda suceder antes de 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 código multiproceso, como regla, es imposible decir qué eventos ocurrirán en paralelo, por lo que es imposible describir con certeza el estado global en un momento dado.

Un programa multiproceso con un extenso estado globalmente mutable es uno de los ejemplos más elocuentes del principio de incertidumbre de Heisenberg que conozco. Es imposible comprobar el estado de un programa sin cambiar su comportamiento.

Cuando empiezo otra filípica sobre el estado global mutable (la esencia se describe 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 sabe esto, ¿por qué no puede saberlo por su código? Los programas están repletos de estados mutables globales y los programadores se preguntan por qué el código no funciona.

Como era de esperar, el trabajo más importante en la programación multiproceso ocurre durante la fase de diseño. Se requiere definir claramente qué debe hacer el programa, desarrollar módulos independientes para realizar todas las funciones, describir en detalle qué datos se requieren para qué módulo y determinar las formas de intercambio de información entre los módulos ( Sí, no olvides preparar bonitas camisetas para todos los involucrados en el proyecto. Lo primero.- aprox. ed. en 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 un solo subproceso, es limitar las interacciones entre módulos. Si puede deshacerse del estado mutable compartido, los problemas de intercambio de datos simplemente no surgirán.

Alguien podría argumentar que a veces no hay tiempo para un diseño tan delicado del programa, que permitirá prescindir del estado global. Creo que es posible y necesario dedicar tiempo a esto. Nada afecta a los programas multiproceso de manera tan destructiva como tratar de hacer frente a un estado global mutable. Cuantos más detalles tenga que administrar, es más probable que su programa alcance su punto máximo y se bloquee.

En aplicaciones realistas, debe haber algún tipo de estado compartido que pueda cambiar. Y aquí es donde la mayoría de los programadores comienzan a tener problemas. El programador ve que aquí se requiere un estado compartido, recurre al arsenal multiproceso y toma de allí la herramienta más simple: un bloqueo universal (sección crítica, mutex, o como lo llamen). Parece que creen que la exclusión mutua resolverá todos los problemas de intercambio de datos.

La cantidad de problemas que pueden surgir con una cerradura tan única es asombrosa. Se deben tener en cuenta las condiciones de carrera, los problemas de activación con bloqueos demasiado extensos y los problemas de equidad en la asignación son solo algunos ejemplos. Si tiene varios bloqueos, especialmente si están anidados, también deberá tomar medidas contra los interbloqueos, los interbloqueos dinámicos, las colas de bloqueo y otras amenazas de concurrencia. Además, existen problemas inherentes de bloqueo único.
Cuando escribo o reviso código, tengo una regla práctica casi a prueba de fallas: si hiciste un candado, aparentemente cometiste un error en alguna parte.

Esta afirmación se puede comentar de dos formas:

  1. Si necesita bloquear, probablemente tenga un estado mutable global que desee proteger contra actualizaciones simultáneas. La presencia de un estado mutable global es una falla en la fase de diseño de la aplicación. Revisión y rediseño.
  2. Usar bloqueos correctamente no es fácil y localizar errores relacionados con el bloqueo puede ser increíblemente difícil. Es muy probable que esté utilizando la cerradura 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 generalmente encuentro problemas en eso.

Ambas interpretaciones son correctas.

Escribir código multiproceso es fácil. Pero es muy, muy difícil utilizar correctamente las primitivas de sincronización. Es posible que no esté calificado para usar ni siquiera un candado correctamente. 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 concurrente mucho mejor que usted utilizan estas primitivas para construir estructuras de datos concurrentes y construcciones de sincronización de alto nivel. Y tú y yo, programadores ordinarios, simplemente tomamos esas construcciones y las usamos en nuestro código. Un programador de aplicaciones no debería utilizar primitivas de sincronización de bajo nivel con más frecuencia de lo que realiza llamadas directas a los controladores de dispositivos. Es decir, casi nunca.

Intentar usar candados para resolver problemas de intercambio de datos es como apagar un fuego con oxígeno líquido. Como 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 el subproceso múltiple es irrelevante

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

Imagínese diciéndole a un adolescente cómo montar un motor de combustión interna por sí mismo. Luego, sin ningún entrenamiento en la conducción, lo pones al volante de un automóvil y le dices: "¡Ve!" El adolescente entiende cómo funciona un automóvil, pero no tiene idea de cómo ir del punto A al punto B en él.

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

La literatura introductoria sobre subprocesos (y los cursos académicos relacionados) no deben explorar tales construcciones de bajo nivel. Debe concentrarse en resolver las clases de problemas más comunes y mostrar a los desarrolladores cómo se resuelven estos problemas utilizando capacidades de alto nivel. En principio, la mayoría de las aplicaciones comerciales son programas extremadamente simples. Leen datos de uno o más dispositivos de entrada, realizan un procesamiento complejo de estos datos (por ejemplo, en el proceso, solicitan algunos datos más) y luego muestran los resultados.

Estos programas a menudo encajan perfectamente en el modelo proveedor-consumidor, que requiere solo tres subprocesos:

  • el flujo de entrada lee los datos y los coloca en la cola de entrada;
  • un hilo de trabajo lee registros de la cola de entrada, los procesa y coloca los resultados en la cola de salida;
  • el flujo de salida lee las entradas de la cola de salida y las almacena.

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

Si bien técnicamente estas colas se pueden considerar como zonas de estado compartido, en la práctica son solo canales de comunicación en los que opera su propia sincronización interna. Las colas permiten trabajar con muchos productores y consumidores a la vez, y puede agregarles y quitarles elementos en paralelo.

Dado que las etapas de entrada, procesamiento y salida están aisladas entre sí, su implementación se puede cambiar fácilmente sin afectar el resto del programa. Siempre que el tipo de datos en la cola no cambie, puede refactorizar los componentes individuales del programa a su discreción. Además, dado que un número arbitrario de proveedores y consumidores participa en la cola, no es difícil agregar otros productores / consumidores. Podemos tener docenas de flujos 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 datos. En el marco de una sola computadora, este modelo se adapta bien.

Más importante aún, los lenguajes de programación y las bibliotecas modernos facilitan la creación de aplicaciones de productor-consumidor. En .NET, encontrará colecciones paralelas y la biblioteca de flujo de datos TPL. Java tiene el servicio Executor, así como BlockingQueue y otras clases del espacio de nombres java.util.concurrent. C ++ tiene una biblioteca de subprocesos Boost y la biblioteca de bloques de construcción de subprocesos de Intel. Visual Studio 2013 de Microsoft presenta agentes asincrónicos. Hay bibliotecas similares en Python, JavaScript, Ruby, PHP y, hasta donde yo sé, en muchos otros lenguajes. Puede crear una aplicación productor-consumidor utilizando cualquiera de estos paquetes, sin tener que recurrir a bloqueos, semáforos, variables de condición o cualquier otra primitiva de sincronización.

En estas bibliotecas se utilizan libremente una amplia variedad de primitivas de sincronización. Esto esta bien. Todas estas bibliotecas 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 prácticamente lo mismo que usar una biblioteca de idiomas en tiempo de ejecución. Se puede comparar con la programación en un lenguaje de alto nivel en lugar de en lenguaje ensamblador.

El modelo proveedor-consumidor es solo uno de los muchos ejemplos. Las bibliotecas anteriores contienen clases que se pueden usar para implementar muchos de los patrones de diseño de subprocesos comunes sin entrar en detalles de bajo nivel. Es posible crear aplicaciones multiproceso a gran escala sin preocuparse de 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 solo adquieren importancia cuando están involucrados varios subprocesos simultáneos. Si descuida estos aspectos importantes, incluso el conocimiento más completo de subprocesos 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 está sucediendo a nivel de sistema. Cuanto más complejas se vuelven las aplicaciones, los detalles más complejos deben ocultarse entre los niveles de API. Hemos estado haciendo esto durante más de una docena de años. Se puede argumentar que el ocultamiento cualitativo de la complejidad del sistema al programador es la razón principal por la que el programador es capaz de escribir aplicaciones modernas. De hecho, ¿no estamos ocultando la complejidad del sistema mediante la implementación del bucle de mensajes de la interfaz de usuario, la creación de protocolos de comunicación de bajo nivel, etc.?

La situación es similar con el multiproceso. La mayoría de los escenarios de subprocesos múltiples que puede encontrar el programador de aplicaciones comerciales promedio ya son bien conocidos y están bien implementados en las bibliotecas. Las funciones de la biblioteca hacen un gran trabajo al ocultar la asombrosa complejidad del paralelismo. Debe aprender a usar estas bibliotecas de la misma manera que usa bibliotecas de elementos de interfaz de usuario, protocolos de comunicación y muchas otras herramientas que simplemente funcionan. Deje el subproceso múltiple de bajo nivel a los especialistas: los autores de las bibliotecas utilizadas en la creación de aplicaciones.

fin del documento. De esta forma, las entradas de registro realizadas por diferentes procesos nunca se mezclan. Los sistemas Unix más modernos proporcionan un servicio syslog especial (3C) para el registro.

Ventajas:

  1. Facilidad de desarrollo. De hecho, ejecutamos muchas copias de una aplicación de un solo subproceso y se ejecutan de forma independiente entre sí. Es posible no utilizar ninguna API multiproceso específica y medios de comunicación entre procesos.
  2. Alta fiabilidad. La terminación anormal de cualquiera de los procesos no afecta al resto de los procesos de ninguna manera.
  3. Buena portabilidad. La aplicación funcionará en cualquier SO multitarea
  4. Alta seguridad. Se pueden ejecutar diferentes procesos de aplicación en nombre de diferentes usuarios. Así, se puede implementar el principio de privilegio mínimo, cuando cada uno de los procesos tiene solo aquellos derechos que son necesarios para que funcione. Incluso si se encuentra un error en alguno de los procesos que permite la ejecución remota de código, un atacante solo podrá obtener el nivel de acceso con el que se ejecutó este proceso.

Desventajas:

  1. No todas las aplicaciones se pueden proporcionar de esta manera. Por ejemplo, esta arquitectura es adecuada para un servidor que sirve páginas HTML estáticas, pero no para un servidor de base de datos y muchos servidores de aplicaciones.
  2. Crear y destruir procesos es una operación costosa, por lo que esta arquitectura no es óptima para muchas tareas.

Los sistemas Unix toman una serie de pasos para hacer que la creación de un proceso y el inicio de un nuevo programa en un proceso sea lo más barato posible. Sin embargo, debe comprender que crear un hilo dentro de un proceso existente siempre será más económico que crear un proceso nuevo.

Ejemplos: apache 1.x (servidor HTTP)

Aplicaciones de multiprocesamiento que se comunican a través de enchufes, tuberías y colas de mensajes de IPC de System V

Los medios enumerados de IPC (comunicación entre procesos) se refieren a los llamados medios de comunicación entre procesos armónicos. Le permiten organizar la interacción de procesos e hilos sin usar memoria compartida. A los teóricos de la programación les gusta mucho esta arquitectura porque virtualmente elimina muchas de las opciones para errores de competencia.

Ventajas:

  1. Relativa facilidad de desarrollo.
  2. Alta fiabilidad. La terminación anormal de uno de los procesos hace que la tubería o el conector se cierre y, en el caso de las colas de mensajes, los mensajes dejan de ingresar o recuperarse de la cola. El resto de los procesos de la aplicación pueden detectar fácilmente este error y recuperarse, quizás (pero no necesariamente) simplemente reiniciando el proceso fallido.
  3. Muchas de estas aplicaciones (especialmente las basadas en sockets) se rediseñan fácilmente para ejecutarse en un entorno distribuido, donde diferentes componentes de la aplicación se ejecutan en diferentes máquinas.
  4. Buena portabilidad. La aplicación funcionará en la mayoría de los sistemas operativos multitarea, incluidos los sistemas Unix más antiguos.
  5. Alta seguridad. Se pueden ejecutar diferentes procesos de aplicación en nombre de diferentes usuarios. Así, se puede implementar el principio de privilegio mínimo, cuando cada uno de los procesos tiene solo aquellos derechos que son necesarios para que funcione.

Incluso si se encuentra un error en alguno de los procesos que permite la ejecución remota de código, un atacante solo podrá obtener el nivel de acceso con el que se ejecutó este proceso.

Desventajas:

  1. Esta arquitectura no es fácil de diseñar e implementar para todas las aplicaciones.
  2. Todos los tipos enumerados de herramientas IPC asumen transmisión de datos en serie. Si se requiere acceso aleatorio a datos compartidos, esta arquitectura es inconveniente.
  3. La transferencia de datos a través de una tubería, un socket y una cola de mensajes requiere que se ejecuten llamadas al sistema y que los datos se copien dos veces: primero desde el espacio de direcciones del proceso original al espacio de direcciones del kernel, luego desde el espacio de direcciones del kernel a la memoria proceso objetivo... Estas son operaciones costosas. Al transferir grandes cantidades de datos, esto puede convertirse en un problema grave.
  4. La mayoría de los sistemas tienen límites en el número total de tuberías, enchufes e instalaciones de IPC. Por ejemplo, Solaris permite un máximo de 1.024 conductos, sockets y archivos abiertos por proceso de forma predeterminada (debido a las limitaciones de la llamada al sistema seleccionada). El límite de arquitectura de Solaris es 65.536 canalizaciones, sockets y archivos por proceso.

    El límite en el número total de sockets TCP / IP no es más de 65536 por interfaz de red (debido al formato de los encabezados TCP). Las colas de mensajes de System V IPC están ubicadas en el espacio de direcciones del kernel, por lo que existen límites estrictos en la cantidad de colas en el sistema y en la cantidad y cantidad de mensajes en cola simultáneamente.

  5. Crear y destruir un proceso y cambiar entre procesos son operaciones costosas. Esta arquitectura no es óptima en todos los casos.

Aplicaciones de multiprocesamiento de memoria compartida

La memoria compartida puede ser memoria compartida System V IPC y mapeo de archivo a memoria. Para sincronizar el acceso, puede utilizar semáforos, mutexes y semáforos POSIX de System V IPC y, al mapear archivos a la memoria, capturar secciones del archivo.

Ventajas:

  1. Acceso aleatorio eficiente a datos compartidos. Esta arquitectura es adecuada para implementar servidores de bases de datos.
  2. Alta tolerancia. Se puede migrar a cualquier sistema operativo que admita o emule System V IPC.
  3. Seguridad relativamente alta. Se pueden ejecutar diferentes procesos de aplicación en nombre de diferentes usuarios. Así, se puede implementar el principio de privilegio mínimo, cuando cada uno de los procesos tiene solo aquellos derechos que son necesarios para que funcione. Sin embargo, la separación de niveles de acceso no es tan estricta como en las arquitecturas consideradas anteriormente.

Desventajas:

  1. La relativa complejidad del desarrollo. Los errores de sincronización de acceso, los llamados errores de contención, son muy difíciles de detectar durante las pruebas.

    Esto puede resultar en un aumento de 3 a 5 veces en el costo total de desarrollo en comparación con arquitecturas multitarea de un solo subproceso o más simples.

  2. Baja fiabilidad. La terminación anormal de cualquiera de los procesos de la aplicación puede dejar (y a menudo deja) la memoria compartida en un estado inconsistente.

    Esto a menudo hace que el resto de la aplicación se bloquee. Algunas aplicaciones, como Lotus Domino, matan intencionalmente los procesos de todo el servidor cuando alguno de ellos termina de forma anormal.

  3. Crear y destruir un proceso y cambiar entre ellos son operaciones costosas.

    Por tanto, esta arquitectura no es óptima para todas las aplicaciones.

  4. En determinadas circunstancias, el uso de la memoria compartida puede provocar una escalada de privilegios. Si se encuentra un error en uno de los procesos que conduce a la ejecución remota de código, es muy probable que un atacante pueda usarlo para ejecutar código de forma remota en otros procesos de la aplicación.

    Es decir, en el peor de los casos, un atacante puede obtener el nivel de acceso correspondiente al más alto de los niveles de acceso de los procesos de la aplicación.

  5. Las aplicaciones de memoria compartida deben ejecutarse en la misma computadora física, o al menos en máquinas que tienen RAM compartida. De hecho, esta limitación se puede eludir, por ejemplo, mediante el uso de archivos compartidos asignados en memoria, pero esto introduce una sobrecarga significativa.

De hecho, esta arquitectura combina las desventajas de las aplicaciones multiproceso y multiproceso propiamente dichas. Sin embargo, una serie de aplicaciones populares desarrolladas en los años 80 y principios de los 90, antes de las API multiproceso estandarizadas de Unix, utilizan esta arquitectura. Se trata de muchos servidores de bases de datos, tanto comerciales (Oracle, DB2, Lotus Domino) como de software gratuito, versiones modernas de Sendmail y algunos otros servidores de correo.

Aplicaciones multiproceso adecuadas

Los subprocesos o subprocesos de una aplicación se ejecutan dentro de un solo proceso. Todo el espacio de direcciones de un proceso se comparte entre subprocesos. A primera vista, parece que esto le permite organizar la interacción entre subprocesos sin ninguna API especial. En realidad, este no es el caso: si varios subprocesos funcionan con una estructura de datos compartida o un recurso del sistema, y ​​al menos uno de los subprocesos modifica esta estructura, en algunos momentos los datos serán inconsistentes.

Por lo tanto, los hilos deben utilizar medios especiales para organizar la interacción. Las herramientas más importantes son las primitivas de exclusión mutua (mutex y bloqueos de lectura / escritura). Usando estas primitivas, el programador puede asegurarse de que ningún subproceso acceda a recursos compartidos mientras están en un estado inconsistente (esto se llama exclusión mutua). System V IPC, solo se comparten aquellas estructuras que están asignadas en el segmento de memoria compartida. Las variables regulares y las estructuras de datos dinámicas normalmente asignadas son diferentes para cada proceso). Los errores de acceso a datos compartidos (errores de competencia) son muy difíciles de detectar durante las pruebas.

  • El alto costo de desarrollar y depurar aplicaciones debido a la cláusula 1.
  • Baja fiabilidad. La destrucción de estructuras de datos, como por desbordamiento de búfer o errores de puntero, afecta a todos los subprocesos de un proceso y, por lo general, da como resultado una terminación anormal de todo el proceso. Otros errores fatales, como la división por cero en uno de los subprocesos, también suelen hacer que todos los subprocesos del proceso se bloqueen.
  • Baja seguridad. Todos los subprocesos de la aplicación se ejecutan en un proceso, es decir, en nombre del mismo usuario y con los mismos derechos de acceso. Es imposible implementar el principio de los privilegios mínimos necesarios, el proceso debe ser ejecutado en nombre del usuario que puede realizar todas las operaciones requeridas por todos los hilos de la aplicación.
  • La creación de un hilo sigue siendo una operación bastante cara. Para cada subproceso, se asigna necesariamente su propia pila, que de forma predeterminada ocupa 1 megabyte de RAM en arquitecturas de 32 bits y 2 megabytes en arquitecturas de 64 bits, y algunos otros recursos. Por tanto, esta arquitectura no es óptima para todas las aplicaciones.
  • La imposibilidad de ejecutar la aplicación en un sistema informático de varias máquinas. Las técnicas mencionadas en la sección anterior, como la asignación de archivos compartidos a la memoria, no son aplicables a un programa multiproceso.
  • En general, podemos decir que las aplicaciones multiproceso tienen casi las mismas ventajas y desventajas que las aplicaciones multiprocesamiento que utilizan memoria compartida.

    Sin embargo, el costo de ejecutar una aplicación multiproceso es menor y el desarrollo de dicha aplicación es, en algunos aspectos, más fácil que una aplicación basada en memoria compartida. Por lo tanto, en los últimos años, las aplicaciones multiproceso se han vuelto cada vez más populares.

    Capítulo 10.

    Aplicaciones multiproceso

    La multitarea en los sistemas operativos modernos se da por sentada [ Antes de Apple OS X, las computadoras Macintosh no tenían sistemas operativos multitarea modernos. Es muy difícil diseñar correctamente un sistema operativo con multitarea en toda regla, por lo que OS X tenía que basarse en el sistema Unix.]. El usuario espera que cuando el editor de texto y el cliente de correo se inicien al mismo tiempo, estos programas no entren en conflicto y, al recibir correo electrónico, el editor no deje de funcionar. Cuando se inician 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 haya varios procesadores instalados en la computadora). Como resultado, espejismo ejecutar varios programas al mismo tiempo, porque incluso el mejor mecanógrafo (y la conexión a Internet más rápida) no puede seguir el ritmo de un procesador moderno.

    El subproceso múltiple, 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, un cliente de correo electrónico de subprocesos múltiples 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 subproceso múltiple.

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

    Sin embargo, el multiproceso no es una de las características que se implementan fácilmente en los lenguajes de programación y que los programadores dominan fácilmente. ¿Por qué?

    Porque en aplicaciones multiproceso, pueden ocurrir errores muy complicados que aparecen y desaparecen de manera impredecible (y tales errores son los más difíciles de depurar).

    Una advertencia: el multihilo es 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 elusivos, 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 demuestren errores comunes. Este es el enfoque más seguro para aprender programación multiproceso: debería poder detectar problemas potenciales cuando todo parece estar funcionando bien a primera vista y saber cómo resolverlos. Si desea utilizar técnicas de programación de subprocesos múltiples, no puede prescindir de ellas.

    Este capítulo sentará una base sólida para un trabajo independiente posterior, pero no podremos describir la programación multiproceso en todas las complejidades; solo la documentación impresa sobre las clases del espacio de nombres Threading tiene más de 100 páginas. Si desea dominar la programación multiproceso a un nivel superior, consulte libros especializados.

    Pero no importa cuán peligrosa sea la programación multiproceso, es indispensable para la solución profesional de algunos problemas. Si sus programas no utilizan subprocesos múltiples cuando sea apropiado, los usuarios se sentirán muy frustrados y preferirán otro producto. Por ejemplo, solo en la cuarta versión del popular programa de correo electrónico Eudora aparecieron las capacidades de subprocesos múltiples, sin las cuales es imposible imaginar un programa moderno para trabajar con 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, en .NET, los programas de un solo subproceso simplemente no existen. Todo Los programas .NET son multiproceso porque el recolector de basura se ejecuta como un proceso en segundo plano de baja prioridad. Como se muestra a continuación, para una programación gráfica seria en .NET, el subproceso adecuado ayuda a evitar que la interfaz gráfica se bloquee cuando el programa está ejecutando operaciones largas.

    Presentación de subprocesos múltiples

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

    Guardar contexto conlleva un costo de tiempo y memoria. El sistema operativo recuerda el estado del subproceso del programa y transfiere el control a otro subproceso. Cuando el programa quiere continuar ejecutando el hilo suspendido, el contexto guardado debe ser restaurado, lo que lleva aún más tiempo. Por lo tanto, el subproceso múltiple solo debe usarse cuando los beneficios compensen todos los costos. A continuación se enumeran algunos ejemplos típicos.

    • La funcionalidad del programa se divide de forma clara y natural en varias operaciones heterogéneas, como en el ejemplo de recepción de correo electrónico y preparación de nuevos mensajes.
    • El programa realiza cálculos largos y complejos, y no desea que la interfaz gráfica se bloquee mientras duren 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 el número de subprocesos activos no exceda el número de procesadores, la ejecución en paralelo está prácticamente libre de los costos asociados con la conmutación de subprocesos).

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

    Se ejecutará un procedimiento, no un objeto, en el flujo del programa.

    Es difícil decir qué se entiende por la expresión "el objeto se está ejecutando", pero uno de los autores a menudo imparte seminarios sobre programación de subprocesos múltiples y esta pregunta se hace con más frecuencia que otros. Quizás alguien piense que el trabajo del hilo del programa comienza con una llamada al método New de la clase, después de lo cual el hilo 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 del objeto son transmitidos y recibidos por varios subprocesos diferentes (por cierto, esta es una de las razones que complican la programación multiproceso: para depurar un programa, ¡necesita averiguar qué hilo en un momento dado realiza este o aquel procedimiento!).

    Dado que los subprocesos se crean a partir de métodos de objetos, el objeto en sí generalmente se crea antes que el subproceso. Después de crear con éxito el objeto, el programa crea un hilo, pasándole la dirección del método del objeto y solo después de eso da la orden de iniciar la ejecución del hilo. El procedimiento para el que se creó el hilo, 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 alcance.

    Los métodos comunes de clases también se pueden ejecutar en subprocesos de programa. En este caso, también hay que tener en cuenta otra circunstancia importante: el hilo finaliza con una salida del procedimiento para el que fue creado. La finalización normal del flujo del programa no es posible hasta que se sale del procedimiento.

    Los hilos 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 transmisiones para obtener más información.

    Las características principales de .NET relacionadas con el uso de subprocesos programáticos se concentran en el espacio de nombres Threading. Por lo tanto, la mayoría de los programas multiproceso deben comenzar con la siguiente línea:

    Sistema de Importaciones.

    Importar un espacio de nombres hace que su programa sea más fácil de escribir y habilita la tecnología IntelliSense.

    La conexión directa de los flujos con los procedimientos sugiere que en este cuadro son importantes delegados(ver capítulo 6). Específicamente, el espacio de nombres Threading incluye el delegado ThreadStart, que normalmente se usa al iniciar subprocesos de programa. La sintaxis para usar este delegado se ve así:

    Subproceso de subproceso de delegado público ()

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

    Dominios de aplicación

    Los subprocesos .NET se ejecutan en los denominados dominios de aplicación, definidos en la documentación como "la zona de pruebas en la que se ejecuta la aplicación". Se puede pensar en un dominio de aplicación como una versión ligera de los procesos Win32; un solo proceso 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 (en la documentación, los dominios de aplicación también se comparan con los procesos lógicos que se ejecutan dentro de un proceso físico). En NET, toda la administración de la memoria está a cargo del tiempo de ejecución, por lo que se pueden ejecutar varios dominios de aplicación en un solo proceso de Win32. Uno de los beneficios de este esquema es la mejora de las capacidades de escalado de las aplicaciones. Las herramientas para trabajar con dominios de aplicaciones se encuentran en la clase AppDomain. Le recomendamos que estudie la documentación de esta clase. Con su ayuda, puede obtener información sobre el entorno en el que se ejecuta su programa. Específicamente, la clase AppDomain se usa cuando se realiza una reflexión sobre clases de sistema .NET. El siguiente programa enumera los ensamblajes cargados.

    Sistema de Importaciones Reflexión

    Módulo Modulel

    Sub principal ()

    Atenuar el dominio como AppDomain

    theDomain = AppDomain.CurrentDomain

    Dim Assemblies () como

    Ensamblados = theDomain.GetAssemblies

    Dim anAssemblyxAs

    Para cada ensamblaje en ensamblajes

    Console.WriteLinetanAssembly.Full Name) Siguiente

    Console.ReadLine ()

    End Sub

    Módulo final

    Creando arroyos

    Comencemos con un ejemplo rudimentario. Digamos que desea ejecutar un procedimiento en un subproceso separado que disminuye el valor del contador en un ciclo infinito. El procedimiento se define como parte de la clase:

    Clase pública WillUseThreads

    SubtractFromCounter público ()

    Dim count como entero

    Hacer mientras es verdadero contar - = 1

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

    & contar)

    Círculo

    End Sub

    Clase final

    Dado que la condición de bucle Do siempre es verdadera, podría pensar que nada interferirá con la ejecución del procedimiento SubtractFromCounter. Sin embargo, en una aplicación multiproceso, este no es siempre el caso.

    El siguiente fragmento contiene el procedimiento Sub Main que inicia el subproceso y el comando Importaciones:

    Opción estricta en el sistema de importaciones. Módulo de subprocesamiento Modulel

    Sub principal ()

    1 Atenuar myTest como nuevo WillUseThreads ()

    2 Dim bThreadStart As New ThreadStart (AddressOf _

    myTest.SubtractFromCounter)

    3 Atenuar bThread como nuevo hilo (bThreadStart)

    4 "bThread.Start ()

    Dim i como entero

    5 Hazlo mientras sea verdadero

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

    Círculo

    End Sub

    Módulo final

    Echemos un vistazo a los puntos más importantes en secuencia. En primer lugar, el procedimiento Sub Man n siempre funciona en convencional(Hilo principal). En los programas .NET, siempre hay al menos dos subprocesos en ejecución: el subproceso principal y el subproceso de recolección de basura. 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 la clase de prueba creada en la línea 1 (este procedimiento se llama sin parámetros). BienAl importar el espacio de nombres Threading, se puede omitir el nombre largo. El nuevo objeto hilo se crea en la línea 3. Observe el paso del delegado ThreadStart al llamar al constructor de la clase Thread. Algunos programadores prefieren concatenar 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 de ThreadStart. Al llamar a este método, le decimos al sistema operativo que el procedimiento Restar debe ejecutarse en un hilo separado.

    La palabra "comienza" en el párrafo anterior está entre comillas, porque esta es una de las muchas rarezas de la programación multiproceso: ¡Llamar al inicio en realidad no inicia el hilo! Simplemente le dice al sistema operativo que programe la ejecución del hilo especificado, pero comenzar directamente está fuera del control del programa. No podrá comenzar a ejecutar subprocesos por su cuenta, porque el sistema operativo siempre controla la ejecución de subprocesos. En una sección posterior, aprenderá cómo usar la prioridad para hacer que el sistema operativo inicie su hilo más rápido.

    En la Fig. 10.1 muestra un ejemplo de lo que puede suceder después de iniciar un programa y luego interrumpirlo con la tecla Ctrl + Pausa. En nuestro caso, ¡un nuevo hilo comenzó solo después de que el contador en el hilo principal aumentó a 341!

    Arroz. 10.1. Tiempo de ejecución de software multiproceso simple

    Si el programa se ejecuta durante un período de tiempo más largo, el resultado será similar al que se muestra en la Fig. 10.2. Vemos que tula finalización del subproceso en ejecución se suspende y el control se transfiere de nuevo al subproceso principal. En este caso, hay una manifestación multiproceso preventivo a través del corte 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 cuantificación del tiempo también resuelve uno de los problemas comunes que surgieron antes en los programas multiproceso: un subproceso ocupa todo el tiempo de la CPU y no es inferior al control de otros subprocesos (como regla, esto ocurre en ciclos intensivos como el anterior). Para evitar el secuestro exclusivo de CPU, sus subprocesos deben transferir el control a otros subprocesos de vez en cuando. Si el programa resulta "inconsciente", hay otra solución ligeramente menos deseable: el sistema operativo siempre se adelanta a un hilo en ejecución, independientemente de su nivel de prioridad, de modo que se concede acceso al procesador a todos los hilos del sistema.

    Debido a que los esquemas de cuantificación de todas las versiones de Windows que ejecutan .NET tienen un intervalo de tiempo mínimo para cada subproceso, en la programación .NET, los problemas con la incautación exclusiva de la CPU no son tan graves. Por otro lado, si alguna vez se adapta el marco .NET para otros sistemas, esto puede cambiar.

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

    bThread.Priority = ThreadPriority.Highest

    Arroz. 10.3. El hilo con la prioridad más alta generalmente comienza más rápido

    Arroz. 10.4. El procesador también se proporciona para subprocesos de menor prioridad

    El comando asigna la máxima prioridad al nuevo hilo y disminuye la prioridad del hilo principal. De la fig. 10.3 se puede ver que el nuevo hilo comienza a funcionar más rápido que antes, pero, como se muestra en la Fig. 10.4, el hilo principal también toma el controlpereza (aunque por muy poco tiempo y solo después de un trabajo prolongado del flujo con sustracción). 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

    ThreadPrlority.Normal

    ThreadPriority.BelowNormal

    ThreadPriority.Lowest

    Método de unión

    A veces, es necesario pausar un hilo de programa hasta que termine otro hilo. Digamos que desea pausar el hilo 1 hasta que el hilo 2 complete su cálculo. Para esto de la secuencia 1 el método Join se llama para la secuencia 2. En otras palabras, el comando

    thread2.Join ()

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

    Si se une a la secuencia 1 a la secuencia 2 mediante el método Unir, el sistema operativo iniciará automáticamente la secuencia 1 después de la secuencia 2. Tenga en cuenta que el proceso de inicio es no determinista: Es imposible decir exactamente cuánto tiempo después de que finalice el hilo 2, el hilo 1. Empezará a funcionar. 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 una vez transcurrido el intervalo de tiempo especificado, lo que hace que el programador del sistema operativo asigne tiempo de CPU al subproceso nuevamente. El método devuelve True si el flujo 2 termina antes de que expire el intervalo de tiempo de espera especificado y False en caso contrario.

    Recuerde la regla básica: si el hilo 2 se ha completado o se ha agotado el tiempo de espera, no tiene control sobre cuándo se activa el hilo 1.

    Nombres de subprocesos, CurrentThread y ThreadState

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

    Aunque hay una maravillosa ventana de subprocesos para depurar aplicaciones multiproceso en VB .NET, que se describe a continuación, muy a menudo nos ayudó el comando

    MsgBox (Thread.CurrentThread.Name)

    Muy a menudo resulta que el código se está ejecutando en un hilo completamente diferente al que se suponía que debía ejecutarse.

    Recuerde que el término "programación no determinista de flujos 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 este motivo, los programas suelen utilizar la propiedad ThreadState para devolver información sobre el estado actual de un hilo.

    Ventana de transmisiones

    La ventana Threads de Visual Studio .NET es invaluable para depurar programas multiproceso. Se activa mediante el comando del submenú Depurar> Windows en modo de interrupción. Digamos que asignó un nombre al hilo bThread con el siguiente comando:

    bThread.Name = "Restando hilo"

    En la Fig. 10.5.

    Arroz. 10.5. Ventana de transmisiones

    La flecha en la primera columna marca el hilo activo devuelto por la propiedad Thread.CurrentThread. La columna de ID contiene ID de hilo numéricos. La siguiente columna enumera los nombres de las transmisiones (si están asignados). La columna Ubicación indica el procedimiento a ejecutar (por ejemplo, el procedimiento WriteLine de la clase Console en la Figura 10.5). Las columnas restantes contienen información sobre la prioridad y los subprocesos suspendidos (consulte la siguiente sección).

    La ventana de subprocesos (¡no el sistema operativo!) Le permite controlar los subprocesos de su programa mediante menús contextuales. Por ejemplo, puede detener el hilo actual haciendo clic con el botón derecho en la línea correspondiente y eligiendo el comando Congelar (puede reanudar el hilo detenido más tarde). La detención de subprocesos se utiliza a menudo durante la depuración para evitar que un subproceso que funciona mal interfiera con la aplicación. Además, la ventana de transmisiones le permite activar otra transmisión (no detenida); para hacer esto, haga clic con el botón derecho en la línea requerida y seleccione el comando Cambiar a hilo del menú contextual (o simplemente haga doble clic en la línea del hilo). Como se mostrará a continuación, esto es muy útil para diagnosticar posibles interbloqueos.

    Suspender una transmisión

    Los flujos no utilizados temporalmente se pueden transferir a un estado pasivo mediante el método Slеer. Una secuencia pasiva también se considera bloqueada. Por supuesto, cuando un subproceso se pone en estado pasivo, el resto de subprocesos tendrá más recursos de procesador. La sintaxis estándar del método Slеer es la siguiente: Thread.Sleep (intervalo_en_milisegundos)

    Como resultado de la llamada de suspensión, el subproceso activo se vuelve pasivo durante al menos un número especificado de milisegundos (sin embargo, no se garantiza la activación inmediatamente después de que haya expirado el intervalo especificado). Tenga en cuenta: al llamar al método, no se pasa una referencia a un hilo específico; el método Sleep se llama solo para el hilo activo.

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

    Thread.Sleep (0)

    La siguiente opción pone el hilo actual en un estado pasivo por un tiempo ilimitado (la activación ocurre solo cuando llamas a Interrupt):

    Thread.Slеer (Timeout.Infinite)

    Dado que los subprocesos pasivos (incluso con un tiempo de espera ilimitado) pueden ser interrumpidos por el método Interrupt, que conduce al inicio de una excepción ThreadlnterruptExcepti, la llamada de Slayer siempre se incluye en un bloque Try-Catch, como en el siguiente fragmento:

    Tratar

    Thread.Sleep (200)

    "Se ha interrumpido el estado pasivo del hilo

    Captura 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 utiliza para suspender programas (si el programa no importa el espacio de nombres Threadipg, debe utilizar el nombre completo Threading.Thread. Sleep).

    Terminar o interrumpir los hilos del programa

    Un hilo terminará automáticamente cuando el método especificado cuando se crea el delegado ThreadStart, pero a veces es necesario terminar el método (y por lo tanto el hilo) cuando ocurren ciertos factores. En tales casos, las corrientes suelen comprobar variable condicional, dependiendo del estado del cualse toma una decisión sobre una salida de emergencia del arroyo. Normalmente, se incluye un bucle Do-While en el procedimiento para esto:

    Sub ThreadedMethod ()

    "El programa debe proporcionar los medios para la encuesta

    "variable condicional.

    "Por ejemplo, una variable condicional se puede diseñar como una propiedad

    Do While conditionVariable = False y más

    "El código principal

    Sub de final de bucle

    Lleva algún tiempo sondear la variable condicional. Solo debe usar el sondeo persistente en una condición de bucle si está esperando que un hilo termine prematuramente.

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

    El acceso a una variable condicional debe estar sincronizado para que la exposición de otros hilos no interfiera 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 de otro modo) no se ejecuta, por lo que la opción de sondear una variable condicional no es adecuada para ellos. En este caso, llame al método Interrupt en la variable de objeto que contiene una referencia al subproceso deseado.

    El método de interrupción solo se puede llamar en subprocesos en estado de espera, suspensión o unión. Si llama a Interrupt para un subproceso que se encuentra en uno de los estados enumerados, luego de un tiempo el subproceso comenzará a funcionar nuevamente y el entorno de ejecución iniciará una excepción ThreadlnterruptedExcepti en el subproceso. Esto ocurre incluso si el hilo se ha hecho pasivo indefinidamente llamando a Thread.Sleepdimeout. Infinito). Decimos "después de un tiempo" porque la programación de subprocesos no es determinista. La excepción ThreadlnterruptedExcepti on es capturada por la sección Catch que contiene el código de salida del estado de espera. Sin embargo, no es necesario que la sección Catch termine el hilo en una llamada de interrupción; el hilo maneja la excepción como mejor le parezca.

    En .NET, el método Interrupt se puede llamar incluso para subprocesos desbloqueados. En este caso, el hilo se interrumpe en el bloqueo más cercano.

    Suspender y matar hilos

    El espacio de nombres Threading contiene otros métodos que interrumpen el subproceso normal:

    • Suspender;
    • Abortar.

    Es difícil decir por qué .NET incluyó soporte para estos métodos: cuando llama a Suspend y Abort, lo más probable es que el programa se vuelva inestable. Ninguno de los métodos permite la desinicialización normal de la secuencia. Además, cuando llama a Suspender o Abortar, no puede predecir en qué estado dejará los objetos el hilo después de ser suspendido o abortado.

    Llamar a Abort lanza una 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 del SDK de .NET:

    “... Cuando un hilo se destruye llamando a Abort, el tiempo de ejecución lanza una ThreadAbortException. Este es un tipo especial de excepción que el programa no puede detectar. Cuando se lanza esta excepción, el tiempo de ejecución ejecuta todos los bloques Finalmente antes de terminar el hilo. Dado que cualquier acción puede tener lugar en los bloques Finalmente, llame a Join para asegurarse de que la transmisión se destruya ".

    Moraleja: No se recomiendan Abortar y Suspender (y si aún no puede prescindir de Suspend, reanude el hilo suspendido utilizando el método Resume). Puede terminar un hilo de forma segura solo sondeando una variable de condición sincronizada o llamando al método de interrupción mencionado anteriormente.

    Subprocesos en segundo plano (demonios)

    Algunos subprocesos que se ejecutan en segundo plano dejan de ejecutarse automáticamente cuando se detienen otros componentes del programa. En particular, el recolector de basura se ejecuta en uno de los subprocesos en segundo plano. Los subprocesos en segundo plano generalmente se crean para recibir datos, pero esto se hace solo si otros subprocesos están ejecutando código que puede procesar los datos recibidos. Sintaxis: nombre del flujo. IsBackGround = True

    Si solo quedan hilos en segundo plano en la aplicación, la aplicación terminará automáticamente.

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

    Recomendamos usar streams solo cuando la funcionalidad del programa esté claramente dividida en varias operaciones. Un buen ejemplo es el extractor de HTML del Capítulo 9. Nuestra clase hace dos cosas: recuperar datos de Amazon y procesarlos. Este es un ejemplo perfecto de una situación en la que la programación multiproceso es verdaderamente apropiada. Creamos clases para varios libros diferentes y luego analizamos los datos en diferentes flujos. La creación de un nuevo hilo para cada libro aumenta la eficiencia del programa, porque mientras un hilo está recibiendo datos (lo que puede requerir esperar en el servidor de Amazon), otro hilo estará ocupado procesando los datos que ya se han recibido.

    La versión multiproceso de este programa funciona de manera más eficiente que la versión 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 se pueden ejecutar en subprocesos, por lo que tendrá que realizar cambios menores en el programa. A continuación se muestra el procedimiento básico, reescrito para excluir parámetros:

    Público Sub FindRank ()

    m_Rank = ScrapeAmazon ()

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

    End Sub

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

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

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

    Se crean cuatro flujos en el mismo bucle en el que se crean los objetos de AmazonRanker:

    Para i = 0 a 3

    Tratar

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

    aThreadStart = New ThreadStar (AddressOf theRanker.FindRan ()

    aThread = Nuevo hilo (aThreadStart)

    aThread.Name = theBook (i.l)

    aThread.Start () Catch e como excepción

    Console.WriteLine (mensaje electrónico)

    Finalizar intento

    próximo

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

    Opción estricta en las importaciones System.IO Imports System.Net

    Sistema de Importaciones.

    Módulo Modulel

    Sub principal ()

    Atenuar el libro (3.1) como cadena

    theBook (0.0) = "1893115992"

    theBook (0.l) = "Programando VB .NET"

    theBook (l.0) = "1893115291"

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

    theBook (2,0) = "1893115623"

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

    theBook (3.0) = "1893115593"

    theBook (3.1) = "Gland la plataforma .Net"

    Dim i como entero

    Atenuar theRanker como = AmazonRanker

    Atenuar aThreadStart como Threading.

    Atenuar un hilo como hilo.

    Para i = 0 a 3

    Tratar

    theRanker = Nuevo AmazonRankerttheBook (i.0). el libro (i.1))

    aThreadStart = New ThreadStart (AddressOf theRanker. FindRank)

    aThread = Nuevo hilo (aThreadStart)

    aThread.Name = theBook (i.l)

    aThread.Start ()

    Captura e como excepción

    Console.WriteLlnete.Message)

    Finalizar Intentar siguiente

    Console.ReadLine ()

    End Sub

    Módulo final

    AmazonRanker de clase pública

    Private m_URL como cadena

    Private m_Rank como entero

    Privado m_Name como cadena

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

    m_URL = "http://www.amazon.com/exec/obidos/ASIN/" e 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 () As String Get

    Si m_Rank<>0 Entonces

    Devolver CStr (m_Rank) Else

    " Problemas

    Terminara si

    Finalizar Obtener

    Propiedad final

    Propiedad pública de solo lectura GetName () As String Get

    Devolver m_Name

    Finalizar Obtener

    Propiedad final

    Función privada ScrapeAmazon () como Integer Integer

    Atenuar la URL como nueva Uri (m_URL)

    Atenuar theRequest como WebRequest

    theRequest = WebRequest.Create (theURL)

    Atenuar theResponse como WebResponse

    theResponse = theRequest.GetResponse

    Atenuar aReader como nuevo StreamReader (theResponse.GetResponseStream ())

    Atenuar los datos como cadena

    theData = aReader.ReadToEnd

    Devolver Analizar (theData)

    Captura E como excepción

    Console.WriteLine (E.Message)

    Console.WriteLine (E.StackTrace)

    Consola. ReadLine ()

    End Try End Function

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

    Atenuar la ubicación como ubicación entera = theData.IndexOf (" Amazon.com

    Rango de ventas:") _

    + "Clasificación de ventas de Amazon.com:".Largo

    Dim temp como cadena

    Hacer hasta que el Data.Substring (Location.l) = "<" temp = temp

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

    Volver Clnt (temp)

    Función final

    Clase final

    Las operaciones de subprocesos múltiples se utilizan comúnmente en los espacios de nombres de .NET y E / S, por lo que la biblioteca de .NET Framework proporciona métodos asincrónicos especiales para ellas. Para obtener más información sobre el uso de métodos asincrónicos al escribir programas multiproceso, 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: Nuestras transmisiones no cambiaron los datos generales. Si permite el cambio en los datos generales, los errores potenciales comienzan a multiplicarse exponencialmente y se vuelve mucho más difícil deshacerse de ellos para el programa. Por otro lado, si prohíbe la modificación de datos compartidos por diferentes subprocesos, la programación .NET de subprocesos múltiples difícilmente diferirá de las capacidades limitadas de VB6.

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

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

    Thread.Sleep (200)

    Catch tie As ThreadlnterruptedException

    "Se ha interrumpido la espera pasiva

    Captura e como excepción

    "Otras excepciones de End Try

    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 ha alcanzado los 105 grados Fahrenheit (40.5 grados Celsius)!

    1 opción estricta en

    2 Sistema de Importaciones.

    Módulo de 3 módulos

    4 Sub Principal ()

    5 Dim myHouse como casa nueva (l0)

    6 Consola. ReadLine ()

    7 End Sub

    8 Módulo final

    9 Casa de clase pública

    10 Const pública MAX_TEMP como entero = 75

    11 mCurTemp privado como entero = 55

    12 habitaciones privadas () como habitación

    13 Public Sub New (ByVal numOfRooms como entero)

    14 ReDim mRooms (numOfRooms = 1)

    15 Dim i como entero

    16 Atenuar un ThreadStart como Threading.

    17 Atenuar un hilo como hilo

    18 Para i = 0 a numOfRooms -1

    19 Prueba

    20 mRooms (i) = NewRoom (Yo, mCurTemp, CStr (i) y "throom")

    21 aThreadStart - Nuevo ThreadStart (AddressOf _

    mRooms (i) .CheckTempInRoom)

    22 aThread = Nuevo hilo (aThreadStart)

    23 aThread.Start ()

    24 Captura E como excepción

    25 Console.WriteLine (E.StackTrace)

    26 Fin del intento

    27 Siguiente

    28 End Sub

    29 Propiedad pública HouseTemp () como entero

    treinta . Obtener

    31 Devolver mCurTemp

    32 Fin Obtener

    33 Establecer (valor ByVal como entero)

    34 mCurTemp = Valor 35 Conjunto final

    36 Fin de la propiedad

    37 Clase final

    38 Sala de clase pública

    39 mCurTemp privado 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 = temp

    45 mName = roomName

    46 End Sub

    47 Sub CheckTempInRoom público ()

    48 Cambio de temperatura ()

    49 End Sub

    50 Sub Cambio de temperatura privado ()

    51 Prueba

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

    53 Hilo. Sueño (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 Hilo. Sueño (200)

    59 mHouse.HouseTemp + = 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 hacer nada, la temperatura es normal

    66 Fin si

    67 Capturar tae como ThreadlnterruptedException

    68 "Se ha interrumpido la espera pasiva

    69 Captura e como excepción

    70 "Otras excepciones

    71 Fin del intento

    72 End Sub

    73 Clase final

    Arroz. 10.6. Problemas de subprocesos múltiples

    El procedimiento Sub Main (líneas 4-7) crea una "casa" con diez "habitaciones". La clase House establece una temperatura máxima de 75 grados Fahrenheit (aproximadamente 24 grados Celsius). Las líneas 13-28 definen un constructor de casas bastante complejo. La clave para entender el programa son las líneas 18-27. La línea 20 crea otro objeto de habitación y se pasa 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 inician diez corrientes para ajustar la temperatura en cada habitación. La clase Room se define en las líneas 38-73. Casa coxpa referenciase almacena 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 verá pronto, ¡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.

    Casi nadie estaría de acuerdo en vivir en temperaturas de 105 grados Fahrenheit (40,5 a 24 grados Celsius). ¿Qué sucedió? El problema está relacionado con la siguiente línea:

    Si mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

    Y sucede lo siguiente: primero, el flujo 1 controla la temperatura. Él ve que la temperatura es demasiado baja y la eleva 5 grados. Desafortunadamente, antes de que suba la temperatura, el flujo 1 se interrumpe y el control se transfiere al flujo 2. El flujo 2 verifica la misma variable que aún no ha sido cambiado flujo 1. Por lo tanto, el flujo 2 también se está preparando para elevar la temperatura en 5 grados, pero no tiene tiempo para hacerlo y también entra en un estado de espera. El proceso continúa hasta que se activa el flujo 1 y pasa al siguiente comando: aumentar la temperatura en 5 grados. El aumento se repite cuando se activan las 10 corrientes, y los vecinos de la casa lo pasarán mal.

    Solución al problema: sincronización

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

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

    son procesados ​​por completo 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 hilo sin interrupción, como una unidad atómica. El planificador de subprocesos no puede interrumpir un grupo de comandos, combinados en un bloque atómico, hasta que se complete. Cualquier lenguaje de programación multiproceso tiene sus propias formas de garantizar la atomicidad. En VB .NET, la forma más fácil de usar el comando SyncLock es pasar una variable de objeto cuando se llama. Realice pequeños cambios 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

    Thread.Sleep (200)

    mHouse.HouseTemp + = 5

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

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

    Elself

    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 empate como ThreadlnterruptedException

    "La espera pasiva fue interrumpida por Catch e As Exception

    "Otras excepciones

    Finalizar intento

    Finalizar SyncLock

    End Sub

    El código del bloque SyncLock se ejecuta de forma atómica. 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 permanece hasta que el subproceso se interrumpe o se reanuda.

    El uso correcto del comando SyncLock mantiene el hilo de su programa seguro. Desafortunadamente, el uso excesivo de SyncLock tiene un impacto negativo en el rendimiento. La sincronización de código en un programa multiproceso reduce varias veces la velocidad de su trabajo. Sincronice solo el código que necesita y libere el bloqueo lo antes posible.

    Las clases de colección base no son seguras para subprocesos en aplicaciones multiproceso, 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 recopilación deben usarse en programas multiproceso donde la integridad de los datos se vea comprometida.

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

    Condición de clase pública Variable

    Casillero privado compartido como objeto = nuevo objeto ()

    MOK privado compartido como booleano compartido

    Propiedad TheConditionVariable () como booleano

    Obtener

    Volver mOK

    Finalizar Obtener

    Establecer (valor de ByVal como booleano) SyncLock (casillero)

    mOK = Valor

    Finalizar SyncLock

    Conjunto final

    Propiedad final

    Clase final

    Clase de monitor y comando SyncLock

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

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

    Para proteger los datos compartidos en una aplicación multiproceso, el comando SyncLock debe sincronizar un objeto a la vez.

    Dado que la sincronización está asociada con un objeto específico, en algunas situaciones, es posible bloquear inadvertidamente otros fragmentos. Digamos que tiene dos métodos sincronizados, el primero y el segundo, los cuales están sincronizados en el objeto bigLock. Cuando el subproceso 1 ingresa al método primero y toma bigLock, ningún subproceso podrá ingresar al método en segundo lugar porque el acceso al mismo 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 personalizable y se puede utilizar para resolver tareas de sincronización no triviales. El comando SyncLock es un análogo cercano de los métodos Enter y Exit de la clase Moni tor:

    Tratar

    Monitor.Enter (theObject) Finalmente

    Monitorear Salir (el Objeto)

    Finalizar intento

    Para algunas operaciones estándar (aumentar / disminuir una variable, intercambiar el 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.

    Enclavamiento

    Durante la sincronización, el bloqueo se establece en objetos, no en subprocesos, por lo que cuando se usa diferente objetos para bloquear diferente Fragmentos de código en programas a veces ocurren errores no triviales. Desafortunadamente, en muchos casos la sincronización en un solo objeto es simplemente inaceptable, ya que conducirá al bloqueo de subprocesos con demasiada frecuencia.

    Considere la situación entrelazado(punto muerto) en su forma más simple. Imagínese dos programadores en la mesa de la cena. Desafortunadamente, solo tienen un cuchillo y un tenedor para dos. Suponiendo que necesita un cuchillo y un tenedor para comer, son posibles dos situaciones:

    • Un programador logra agarrar un cuchillo y un tenedor y comienza a comer. Cuando está lleno, deja la cena reservada y luego otro programador puede tomarlas.
    • Un programador toma el cuchillo y el otro toma el tenedor. Ninguno de los dos puede empezar a comer a menos que el otro abandone su aparato.

    En un programa multiproceso, esta situación se denomina bloqueo mutuo. Los dos métodos están sincronizados en diferentes objetos. El hilo A captura el objeto 1 y entra en la parte del programa protegida por este objeto. Desafortunadamente, para que funcione, necesita acceso a un código protegido por otro bloqueo de sincronización con un objeto de sincronización diferente. Pero antes de que tenga tiempo de ingresar a un fragmento que está sincronizado por otro objeto, el flujo B ingresa y captura este objeto. Ahora el hilo A no puede entrar en el segundo fragmento, el hilo B no puede entrar en el primer fragmento y ambos hilos están condenados a esperar indefinidamente. Ningún subproceso puede continuar ejecutándose porque el objeto requerido nunca se liberará.

    El diagnóstico de puntos muertos se complica por el hecho de que pueden ocurrir en casos relativamente raros. Todo depende del orden en el que el planificador les asigne tiempo de CPU. Es posible que, en la mayoría de los casos, los objetos de sincronización se capturen en un orden no bloqueado.

    La siguiente es una implementación de la situación de interbloqueo que se acaba de describir. Después de una breve discusión de los puntos más fundamentales, mostraremos cómo identificar una situación de interbloqueo en la ventana del hilo:

    1 opción estricta en

    2 Sistema de Importaciones.

    Módulo de 3 módulos

    4 Sub Principal ()

    5 Dim Tom como nuevo programador ("Tom")

    6 Dim Bob como nuevo programador ("Bob")

    7 Atenuar un ThreadStart como nuevo ThreadStart (AddressOf Tom.Eat)

    8 Atenuar un hilo como hilo nuevo (aThreadStart)

    9 aThread.Name = "Tom"

    10 Dim bThreadStart As New ThreadStarttAddressOf Bob.Eat)

    11 Atenuar bThread como nuevo hilo (bThreadStart)

    12 bThread.Name = "Bob"

    13 aThread.Start ()

    14 bThread.Start ()

    15 End Sub

    16 Módulo final

    17 Horquilla de clase pública

    18 mForkAvaiTable privado compartido como booleano = verdadero

    19 Private Shared mOwner As String = "Nadie"

    20 Propiedad privada de solo lectura OwnsUtensil () como cadena

    21 Obtener

    22 Retorno mOwner

    23 Fin Obtener

    24 fin de propiedad

    25 Public Sub GrabForktByVal a As Programmer)

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

    "tratando de agarrar el tenedor").

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

    28 Monitor.Introduzca (Yo) "SyncLock (aFork)"

    29 Si mForkAvailable, entonces

    30 a.HasFork = Verdadero

    31 mOwner = a.MyName

    32 mTenedorDisponible = Falso

    33 Console.WriteLine (a.MyName & "acaba de recibir el fork.waiting")

    34 Prueba

    Thread.Sleep (100) Catch e As Exception Console.WriteLine (e.StackTrace)

    Finalizar intento

    35 Fin si

    36 Monitor.Salida (Yo)

    Finalizar SyncLock

    37 End Sub

    38 Clase final

    39 cuchillo de clase pública

    40 mKnife compartido privado Disponible como booleano = Verdadero

    41 propietario compartido privado como cadena = "Nadie"

    42 Propiedad privada de solo lectura OwnsUtensi1 () como cadena

    43 Obtener

    44 retorno mOwner

    45 Fin Obtener

    46 Fin de la propiedad

    47 Public Sub GrabKnifetByVal a As Programmer)

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

    "tratando de agarrar el cuchillo").

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

    50 Monitor.Introduzca (Yo) "SyncLock (aKnife)"

    51 Si mKnife está disponible, entonces

    52 mKnifeAvailable = Falso

    53 a.HasKnife = Verdadero

    54 mOwner = a.MyName

    55 Console.WriteLine (a.MyName & "acaba de recibir el cuchillo.Esperando")

    56 Prueba

    Thread.Sleep (100)

    Captura e como excepción

    Console.WriteLine (e.StackTrace)

    Finalizar intento

    57 Fin si

    58 Monitor.Salida (Yo)

    59 End Sub

    60 Clase final

    Programador de clase pública 61

    62 Private mName como cadena

    63 mFork privado compartido como bifurcación

    64 mKnife privado compartido como cuchillo

    65 mHasKnife privado como booleano

    66 mHasFork privado como booleano

    67 Sub Compartido Nuevo ()

    68 mFork = New Fork ()

    69 mKnife = Cuchillo nuevo ()

    70 End Sub

    71 Public Sub New (ByVal theName As String)

    72 mNombre = elNombre

    73 End Sub

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

    75 Obtener

    76 Devolver mName

    77 Fin Obtener

    78 Fin de la propiedad

    79 Propiedad pública HasKnife () como booleano

    80 Obtener

    81 Volver mHasKnife

    82 Fin Obtener

    83 Establecer (valor de ByVal como booleano)

    84 mHasKnife = Valor

    85 Juego final

    86 Fin de la propiedad

    87 Propiedad pública HasFork () como booleano

    88 Obtener

    89 Devolución mHasFork

    90 Fin Obtener

    91 Establecer (valor de ByVal como booleano)

    92 mHasFork = Valor

    93 Juego final

    94 Fin de la propiedad

    95 Subcomercio público ()

    96 Hazlo hasta que me.HasKnife y yo.HasFork

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

    98 Si Rnd ()< 0.5 Then

    99 mFork.GrabFork (Yo)

    100 más

    101 mKnife.GrabKnife (Yo)

    102 Fin si

    103 Bucle

    104 MsgBox (Yo.MyName y "¡puedo comer!")

    105 mKnife = Cuchillo nuevo ()

    106 mFork = New Fork ()

    107 End 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 configura; probablemente todo lo que pasa sea comprensible y sin comentarios.

    El código para 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 especifican los valores de los campos comunes, mediante los cuales puede averiguar si el complemento está disponible actualmente y, de no ser así, quién lo está usando. La propiedad ReadOnly OwnUtensi1 (líneas 20-24) está destinada a la transferencia de información más simple. Un elemento central de la clase Fork es el método GrabFork "agarrar la bifurcación", definido en las líneas 25-27.

    1. Las líneas 26 y 27 simplemente imprimen 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 por objetocinturón Me. Dado que nuestro programa solo usa una bifurcación, la sincronización sobre mí garantiza que no haya dos subprocesos que puedan capturarlo al mismo tiempo. El comando Dormir "p (en el bloque que comienza en la línea 34) simula el retraso entre agarrar un tenedor / cuchillo y comenzar una comida. Tenga en cuenta que el comando Dormir no desbloquea objetos y solo acelera los puntos muertos.
      Sin embargo, lo más interesante es el código de la clase Programmer (líneas 61-108). 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 se ejecuta mediante dos subprocesos separados. El proceso continúa en un bucle hasta que un chorro captura el tenedor junto con el cuchillo. En las líneas 98-102, el objeto agarra al azar el tenedor / cuchillo usando la llamada Rnd, que es lo que causa el interbloqueo. Sucede lo siguiente:
      El hilo que ejecuta el método Eat de Tot se invoca y entra en el ciclo. Agarra el cuchillo y entra en un estado de espera.
    2. El hilo que ejecuta el método Eat de Bob invoca y entra en el ciclo. No puede agarrar el cuchillo, pero agarra el tenedor y entra en estado de espera.
    3. El hilo que ejecuta el método Eat de Tot se invoca y entra en el ciclo. Intenta agarrar el tenedor, pero Bob ya lo agarra; el hilo entra en un estado de espera.
    4. El hilo que ejecuta el método Eat de Bob invoca y entra en el ciclo. Intenta agarrar el cuchillo, pero el cuchillo ya está capturado por el objeto Thoth; el hilo entra en un estado de espera.

    Todo esto continúa indefinidamente: nos enfrentamos a una situación típica de punto muerto (intente ejecutar el programa y verá que nadie puede comer de esta manera).
    También puede ver si se ha producido un interbloqueo en la ventana de subprocesos. Ejecute el programa e interrumpa con las teclas Ctrl + Break. Incluya la variable Yo en la ventana gráfica y abra la ventana de arroyos. El resultado se parece al que se muestra en la Fig. 10,7. En la figura, puede ver que el hilo de Bob ha agarrado un cuchillo, pero no tiene tenedor. Haga clic con el botón derecho en la ventana Subprocesos en la línea Tot y seleccione Cambiar a subproceso en el menú contextual. La ventana gráfica muestra que la corriente de Thoth tiene un tenedor, pero no un cuchillo. Por supuesto, esto no es una prueba del cien por cien, pero tal comportamiento al menos hace sospechar que algo andaba mal.
    Si la opción con sincronización por un objeto (como en el programa con un aumento de la temperatura en la casa) no es posible, para evitar bloqueos mutuos, puede numerar los objetos de sincronización y capturarlos siempre en un orden constante. Continuemos con la analogía del programador de comedor: si el hilo siempre toma el cuchillo primero y luego el tenedor, no habrá problemas con el interbloqueo. El primer chorro que agarre el cuchillo podrá comer normalmente. Traducido al lenguaje de los flujos de programas, esto significa que la captura del objeto 2 solo es posible si el objeto 1 se captura primero.

    Arroz. 10,7. Análisis de interbloqueos en la ventana del hilo.

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

    mFork.GrabFork (Yo)

    mKnife.GrabKnife (Yo)

    ¡el interbloqueo desaparece!

    Colaborar en los datos a medida que se crean

    En aplicaciones multiproceso, a menudo hay una situación en la que los subprocesos no solo funcionan con datos compartidos, sino que también esperan a que aparezcan (es decir, el subproceso 1 debe crear datos antes de que el subproceso 2 pueda usarlos). Dado que los datos se comparten, es necesario sincronizar el acceso a ellos. También es necesario proporcionar medios para notificar a los hilos en espera sobre la aparición de datos listos.

    Esta situación se suele llamar el problema proveedor / consumidor. El subproceso está intentando 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 hilo 1 (consumidor) se despierta, ingresa un método sincronizado, busca datos, no los encuentra y entra en un estado de espera. Preliminarmentefísicamente, debe quitar el bloqueo para no interferir con el trabajo del hilo suministrador.
    • El hilo 2 (proveedor) ingresa a un método sincronizado liberado por el hilo 1, crea datos para el flujo 1 y de alguna manera notifica al flujo 1 sobre la presencia de datos. Luego libera el bloqueo para que el hilo 1 pueda procesar los nuevos datos.

    No intente resolver este problema invocando constantemente el subproceso 1 y verificando la condición de la variable de condición, cuyo valor es> establecido por el subproceso 2. Esta decisión afectará seriamente el rendimiento de su programa, ya que en la mayoría de los casos el subproceso 1 ser invocado sin motivo; y el hilo 2 esperará con tanta frecuencia que se quedará sin tiempo para crear datos.

    Las relaciones proveedor / consumidor son muy comunes, por lo que se crean primitivas especiales para tales situaciones en bibliotecas de clases de programación multiproceso. En NET, estas primitivas se denominan Wait y Pulse-PulseAl 1 y forman parte de la clase Monitor. La figura 10.8 ilustra la situación que estamos a punto de programar. El programa organiza tres colas de subprocesos: una cola de espera, una cola de bloqueo y una cola de ejecución. El programador de subprocesos no asigna tiempo de CPU a subprocesos que están en la cola de espera. Para que se 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 cuando se sondea una variable condicional.

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

    "Entrada en un bloque sincronizado del siguiente tipo

    Mientras no hay datos

    Ir a la cola de espera

    Círculo

    Si hay datos, trátelos.

    Dejar bloque sincronizado

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

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

    "Entrar en un bloque de vista sincronizada

    Si bien NO se necesitan datos

    Ir a la cola de espera

    Otros datos de producción

    Cuando 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 un bloque sincronizado (y volver a la cola de ejecución)

    Suponga que nuestro programa simula una familia con un padre que gana dinero y un niño que gasta ese dinero. Cuando se acabe el dineroresulta que 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 en

    2 Sistema de Importaciones.

    Módulo de 3 módulos

    4 Sub Principal ()

    5 Atenúe la familia como una nueva familia ()

    6 theFamily.StartltsLife ()

    7 End Sub

    8 Módulo final

    9

    10 Familia de clase pública

    11 Privado mMoney como entero

    12 mWeek privado como entero = 1

    13 Public Sub StartltsLife ()

    14 Dim aThreadStart As New ThreadStarUAddressOf Me.Produce)

    15 Dim bThreadStart As New ThreadStarUAddressOf Me.Consume)

    16 Atenuar un hilo como hilo nuevo (aThreadStart)

    17 Atenuar bThread como nuevo hilo (bThreadStart)

    18 aThread.Name = "Producir"

    19 aThread.Start ()

    20 bThread.Name = "Consumir"

    21 bHilo. Comienzo ()

    22 End Sub

    23 Propiedad pública TheWeek () como entero

    24 Obtener

    25 Devolución mweek

    26 Fin Obtener

    27 Establecer (valor ByVal como entero)

    28 msemana - Valor

    29 Juego final

    30 Fin de la propiedad

    31 Propiedad pública Nuestro dinero () como entero

    32 Obtener

    33 Devolver mDinero

    34 Fin Obtener

    35 Establecer (valor ByVal como entero)

    36 mDinero = Valor

    37 Juego final

    38 Fin de la propiedad

    39 Subproductos públicos ()

    40 hilos Dormir (500)

    41 Hacer

    42 Monitor.Entrar (Yo)

    43 Hazlo mientras yo.Nuestro dinero> 0

    44 Monitor.Espera (yo)

    45 bucle

    46 Mi dinero = 1000

    47 Monitor.PulseAll (Yo)

    48 Monitor.Salida (Yo)

    49 Bucle

    50 End Sub

    51 Subconsumo público ()

    52 MsgBox ("Estoy en consumir hilo")

    53 Hacer

    54 Monitor.Entrar (Yo)

    55 Hazlo mientras yo.Nuestro dinero = 0

    56 Monitor.Espera (Yo)

    57 Bucle

    58 Console.WriteLine ("Estimado padre, acabo de gastar todo su" & _

    dinero en la semana "y TheWeek)

    59 La semana + = 1

    60 Si TheWeek = 21 * 52 Entonces System.Environment.Salir (0)

    61 Mi dinero = 0

    62 Monitor.PulseAll (Yo)

    63 Monitor.Salida (Yo)

    64 bucle

    65 End Sub

    66 Clase final

    El método StartltsLife (líneas 13-22) se prepara para iniciar las transmisiones Produce y Consume. Lo más importante sucede en los flujos Produce (líneas 39-50) y Consumir (líneas 51-65). El procedimiento Sub Produce comprueba 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 un cambio en la situación. Tenga en cuenta que la llamada a Pulse-Pulse All sólo tiene efecto cuando se libera el bloqueo con el comando Monitor.Salir. Por el contrario, el procedimiento de Subconsumo verifica la disponibilidad de dinero y, si no hay dinero, notifica al padre que está esperando. La línea 60 simplemente termina el programa después de 21 años condicionales; llamando al sistema. Environment.Exit (0) es el análogo .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 que se colocan en la cola de espera deben ser liberados por otras partes de su programa. Es por esta razón que preferimos usar PulseAll sobre Pulse. Dado que no se sabe de antemano qué subproceso se activará cuando se llame a Pulse 1, si hay relativamente pocos subprocesos en la cola, también puede llamar a PulseAll.

    Multithreading en programas gráficos

    Nuestra discusión sobre subprocesos múltiples en aplicaciones GUI comienza con un ejemplo que explica para qué sirven los subprocesos múltiples en aplicaciones GUI. Cree un formulario con dos botones Inicio (btnStart) y Cancelar (btnCancel), como se muestra en la Fig. 10,9. Al hacer clic en el botón Inicio, se genera 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 cadena larga. Tenga en cuenta el uso de la clase StringBuilder para una creación más eficiente de cadenas largas.

    Paso 1

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



    Paso 2

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

    Paso 3

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

    Inmediatamente después de que sale del bloque y libera el bloqueo, el hilo 1 se mueve a la cola de ejecución. Si el hilo 3 llama a Pluse, solo uno ingresa a la cola de ejecuciónsubproceso, cuando se llama Pluse All, todos los subprocesos van a la cola de ejecución.



    Arroz. 10,8. Problema proveedor / consumidor

    Arroz. 10,9. Multithreading en una sencilla aplicación GUI

    Importaciones System.Text

    Personajes aleatorios de clase pública

    Private m_Data como StringBuilder

    Privado mjength, m_count como entero

    Public Sub New (ByVal n como entero)

    m_Length = n -1

    m_Data = Nuevo StringBuilder (m_length) MakeString ()

    End Sub

    Sub privado MakeString ()

    Dim i como entero

    Atenuar myRnd como nuevo aleatorio ()

    Para i = 0 a m_longitud

    "Genere un número aleatorio entre 65 y 90,

    "conviértelo a mayúsculas

    "y adjuntar al objeto StringBuilder

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

    próximo

    End Sub

    Public Sub StartCount ()

    GetEes ()

    End Sub

    GetEes Sub privado ()

    Dim i como entero

    Para i = 0 a m_longitud

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

    m_count + = 1

    Finalizar si es el siguiente

    m_CountDone = Verdadero

    End Sub

    Solo lectura pública

    Propiedad GetCount () como entero Get

    Si no es así (m_CountDone), entonces

    Devolver m_count

    Terminara si

    Fin Obtener fin de propiedad

    Solo lectura pública

    Propiedad IsDone () como Boolean Get

    Regreso

    m_CountDone

    Finalizar Obtener

    Propiedad final

    Clase final

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

    Private Sub btnStart_Click (ByVal sender As System.Object.

    ByVal e As System.EventArgs) Maneja btnSTart.Click

    Dim RC como nuevos personajes aleatorios (10000000)

    RC.StartCount ()

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

    End Sub

    El botón Cancelar muestra un cuadro de mensaje:

    Private Sub btnCancel_Click (ByVal sender As System.Object._

    ByVal e As System.EventArgs) Maneja btnCancel.Click

    MsgBox ("¡Cuenta interrumpida!")

    End Sub

    Cuando se ejecuta el programa y se presiona el botón Iniciar, resulta que el botón Cancelar no responde a la entrada del usuario porque el bucle continuo evita que el botón maneje el evento que recibe. ¡Esto es inaceptable en los programas modernos!

    Hay dos posibles soluciones. La primera opción, bien conocida de las versiones anteriores de VB, prescinde del multiproceso: la llamada DoEvents está incluida en el bucle. En NET, este comando se ve así:

    Application.DoEvents ()

    En nuestro ejemplo, esto definitivamente no es deseable: ¡quién quiere ralentizar un programa con diez millones de llamadas DoEvents! Si, en cambio, asigna el bucle a un subproceso separado, el sistema operativo cambiará entre subprocesos y el botón Cancelar seguirá funcionando. La implementación con un hilo separado se muestra a continuación. Para mostrar claramente que el botón Cancelar funciona, cuando hacemos clic en él, simplemente terminamos el programa.

    Siguiente paso: botón Mostrar recuento

    Digamos que decidió mostrar su imaginación creativa 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. Forma de botón bloqueado

    Se espera que un hilo separado haga el recuento y desbloquee el botón no disponible. Por supuesto, esto se puede hacer; además, esta tarea surge con bastante frecuencia. Desafortunadamente, no podrá actuar de la manera más obvia: vincule el hilo secundario al hilo de la GUI manteniendo un enlace al botón ShowCount en el constructor, o incluso usando un delegado estándar. En otras palabras, Nunca no utilice la siguiente opción (básica erróneo las líneas están en negrita).

    Personajes aleatorios de clase pública

    Privado m_0ata como StringBuilder

    Private m_CountDone como booleano

    Mjength privado. m_count como entero

    Privado m_Button como Windows.Forms.Button

    Public Sub New (ByVa1 n As Integer, _

    ByVal b como Windows.Forms.Button)

    m_longitud = n - 1

    m_Data = Nuevo StringBuilder (mJength)

    m_Button = b MakeString ()

    End Sub

    Sub privado MakeString ()

    Dim I como entero

    Atenuar myRnd como nuevo aleatorio ()

    Para I = 0 a m_length

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

    próximo

    End Sub

    Public Sub StartCount ()

    GetEes ()

    End Sub

    GetEes Sub privado ()

    Dim I como entero

    Para I = 0 a mjength

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

    m_count + = 1

    Finalizar si es el siguiente

    m_CountDone = Verdadero

    m_Button.Enabled = Verdadero

    End Sub

    Solo lectura pública

    Propiedad GetCount () como entero

    Obtener

    Si no es así (m_CountDone), entonces

    Lanzar nueva excepción ("El recuento aún no ha terminado")

    Devolver m_count

    Terminara si

    Finalizar Obtener

    Propiedad final

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

    Obtener

    Devolver m_CountDone

    Finalizar Obtener

    Propiedad final

    Clase final

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

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

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

    Tampoco podrá organizar la interacción de objetos mediante eventos. El trabajador de 06 eventos se ejecuta en el mismo hilo al que se llamó RaiseEvent, por lo que los eventos no le ayudarán.

    Aún así, el sentido común dicta que las aplicaciones gráficas deben proporcionar un medio para modificar elementos de otro hilo. En NET Framework, hay una forma segura para los subprocesos de llamar a métodos de aplicaciones GUI desde otro subproceso. Un tipo especial de delegado de Method Invoker del espacio de nombres System.Windows se utiliza para este propósito. Formularios. El siguiente fragmento muestra una nueva versión del método GetEes (líneas cambiadas en negrita):

    Sub GetEes privado ()

    Dim I como entero

    Para I = 0 a m_length

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

    m_count + = 1

    Finalizar si es el siguiente

    m_CountDone = Verdadero intento

    Dim mylnvoker como nuevo Methodlnvoker (AddressOf UpDateButton)

    myInvoker.Invoke () Catch e como ThreadlnterruptedException

    "Falla

    Finalizar intento

    End Sub

    Public Sub UpDateButton ()

    m_Button.Enabled = Verdadero

    End 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é hay tantos problemas con la programación multiproceso?

    Ahora que comprende algo de multiproceso y los problemas potenciales asociados con él, decidimos que sería apropiado responder la pregunta en el encabezado de esta subsección al final de este capítulo.

    Una de las razones es que el multihilo es un proceso no lineal y estamos acostumbrados a un modelo de programación lineal. Al principio, es difícil acostumbrarse a la idea misma 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: en estos días, los programadores rara vez programan en ensamblador, o al menos miran la salida desensamblada del compilador. De lo contrario, sería mucho más fácil para ellos acostumbrarse a la idea de que docenas de instrucciones de ensamblaje 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, en medio de un comando de alto nivel.

    Pero eso no es todo: los compiladores modernos optimizan el rendimiento del programa y el hardware de la computadora puede interferir con la administración de la memoria. Como consecuencia, 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 la copia cíclica de matrices como para i = 0 an: 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 muchas veces.].

    Con suerte, estas explicaciones le ayudarán a comprender mejor por qué la programación multiproceso causa tantos problemas, ¡o al menos le sorprenderá menos el comportamiento extraño de sus programas multiproceso!

    ¿Qué tema plantea más preguntas y dificultades para los principiantes? Cuando le pregunté a mi maestro y programador de Java, Alexander Pryakhin, sobre esto, respondió de inmediato: “Multithreading”. ¡Gracias a él por la idea y ayuda en la preparación de este artículo!

    Examinaremos el mundo interno de la aplicación y sus procesos, averiguaremos cuál es la esencia del subproceso múltiple, cuándo es útil y cómo implementarlo, usando Java como ejemplo. Si está aprendiendo un idioma de programación orientada a objetos diferente, no se preocupe: los principios básicos son los mismos.

    Sobre los arroyos y sus orígenes

    Para comprender el subproceso múltiple, primero comprendamos qué es un proceso. Un proceso es una parte de la memoria virtual y los recursos que el sistema operativo asigna para ejecutar un programa. Si abre varias instancias de la misma aplicación, el sistema asignará un proceso para cada una. En los navegadores modernos, un proceso independiente puede ser responsable de cada pestaña.

    Probablemente se haya encontrado con el "Administrador de tareas" de Windows (en Linux es "Monitor del sistema") y sepa que los procesos en ejecución innecesarios cargan el sistema, y ​​los más "pesados" a menudo se congelan, por lo que deben terminarse a la fuerza. .

    Pero a los usuarios les encanta la multitarea: no les des pan, solo abre una docena de ventanas y salta de un lado a otro. Existe un dilema: debe garantizar el funcionamiento simultáneo de las aplicaciones y, al mismo tiempo, reducir la carga en el sistema para que no se ralentice. Digamos que el hardware no puede satisfacer las necesidades de los propietarios; es necesario resolver el problema a nivel de software.

    Queremos que el procesador ejecute más instrucciones y procese más datos por unidad de tiempo. Es decir, necesitamos ajustar más código ejecutado en cada segmento de tiempo. Piense en una unidad de ejecución de código como un objeto, eso es un hilo.

    Un caso complejo es más fácil de abordar si lo divide en varios simples. Entonces, cuando se trabaja con memoria: un proceso "pesado" se divide en subprocesos que ocupan menos recursos y es más probable que entreguen el código a la calculadora (cómo exactamente, ver más abajo).

    Cada aplicación tiene al menos un proceso, y cada proceso tiene al menos un subproceso, que se denomina subproceso principal y desde el cual, si es necesario, se lanzan otros nuevos.

    Diferencia entre hilos y procesos

      Los subprocesos utilizan la memoria asignada para el proceso y los procesos requieren su propio espacio de memoria. Por lo tanto, los subprocesos se crean y completan más rápido: el sistema no necesita asignarles un nuevo espacio de direcciones cada vez y luego liberarlo.

      Procesa cada trabajo con sus propios datos: pueden intercambiar algo solo a través del mecanismo de comunicación entre procesos. Los hilos acceden a los datos y recursos de los demás directamente: lo que uno cambió está inmediatamente disponible para todos. El hilo puede controlar al "compañero" en el proceso, mientras que el proceso controla exclusivamente a sus "hijas". Por lo tanto, cambiar entre transmisiones es más rápido y la comunicación entre ellas es más fácil.

    ¿Cuál es la conclusión de esto? Si necesita procesar una gran cantidad de datos lo más rápido posible, divídalos en trozos que puedan ser procesados ​​por subprocesos separados y luego junte el resultado. Es mejor que generar procesos hambrientos de recursos.

    Pero, ¿por qué una aplicación popular como Firefox sigue la ruta de crear múltiples procesos? Porque es para el navegador que las pestañas aisladas funcionan de forma fiable y flexible. Si hay algún problema con un proceso, no es necesario finalizar todo el programa; es posible guardar al menos parte de los datos.

    Que es multiproceso

    Así que llegamos a lo principal. El subproceso múltiple es cuando el proceso de la aplicación se divide en subprocesos que el procesador procesa en paralelo, en una unidad de tiempo.

    La carga computacional se distribuye entre dos o más núcleos, de modo que la interfaz y otros componentes del programa no ralentizan el trabajo de los demás.

    Las aplicaciones de subprocesos múltiples se pueden ejecutar en procesadores de un solo núcleo, pero luego los subprocesos se ejecutan a su vez: el primero funcionó, su estado se guardó, se permitió que el segundo funcionara, se guardó, se devolvió al primero o se lanzó el tercero, etc.

    Las personas ocupadas se quejan de que solo tienen dos manos. Los procesos y programas pueden tener tantas manos como sea necesario para completar la tarea lo más rápido posible.

    Espere una señal: sincronización en aplicaciones multiproceso

    Imagine que varios subprocesos intentan cambiar la misma área de datos al mismo tiempo. ¿Qué cambios serán eventualmente aceptados y quiénes serán cancelados? Para evitar confusiones con los recursos compartidos, los subprocesos deben coordinar sus acciones. Para ello, intercambian información mediante señales. Cada hilo les dice a los demás lo que está haciendo y qué cambios esperar. Entonces, los datos de todos los hilos sobre el estado actual de los recursos están sincronizados.

    Herramientas de sincronización básicas

    Exclusión mutua (exclusión mutua, abreviado como mutex) - una "bandera" que va al hilo que actualmente está permitido para trabajar con recursos compartidos. Elimina el acceso de otros hilos al área de memoria ocupada. Puede haber varias exclusiones mutuas en una aplicación y se pueden compartir entre procesos. Hay un problema: mutex obliga a la aplicación a acceder al kernel del sistema operativo cada vez, lo cual es costoso.

    Semáforo - le permite limitar la cantidad de subprocesos que pueden acceder a un recurso en un momento dado. Esto reducirá la carga en el procesador al ejecutar código donde hay cuellos de botella. El problema es que el número óptimo de subprocesos depende de la máquina del usuario.

    Evento - usted define una condición tras la ocurrencia de la cual el control se transfiere al subproceso deseado. Las transmisiones intercambian datos de eventos para desarrollar y continuar lógicamente las acciones de los demás. Uno recibió los datos, el otro verificó su corrección, el tercero los guardó en el disco duro. Los eventos difieren en la forma en que se cancelan. Si necesita notificar a varios hilos sobre un evento, deberá configurar manualmente la función de cancelación para detener la señal. Si solo hay un hilo de destino, puede crear un evento de reinicio automático. Detendrá la señal en sí misma después de que llegue a la transmisión. Los eventos se pueden poner en cola para un control de flujo flexible.

    Sección crítica - un mecanismo más complejo que combina un contador de bucles y un semáforo. El contador le permite posponer el inicio del semáforo por el tiempo deseado. La ventaja es que el kernel solo se activa si la sección está ocupada y es necesario encender el semáforo. El resto del tiempo, el hilo se ejecuta en modo de usuario. Por desgracia, una sección solo se puede utilizar dentro de un proceso.

    Cómo implementar subprocesos múltiples en Java

    La clase Thread es responsable de trabajar con subprocesos en Java. Crear un nuevo hilo para ejecutar una tarea significa crear una instancia de la clase Thread y asociarla con el código que desee. Esto se puede hacer de dos formas:

      subclase Thread;

      implemente la interfaz Runnable en su clase y luego pase las instancias de la clase al constructor Thread.

    Si bien no tocaremos el tema de los interbloqueos, cuando los hilos bloqueen el trabajo de los demás y se congelen, lo dejaremos para el próximo artículo.

    Ejemplo de subprocesos múltiples de Java: ping pong con mutexes

    Si cree que algo terrible está a punto de suceder, exhale. Consideraremos trabajar con objetos de sincronización casi de una manera divertida: dos subprocesos serán lanzados por un mutex, pero de hecho, verá una aplicación real donde solo un subproceso puede procesar datos disponibles públicamente a la vez.

    Primero, creemos una clase que herede las propiedades del Thread que ya conocemos y escribamos un método kickBall:

    La clase pública PingPongThread extiende Thread (PingPongThread (String name) (this.setName (name); // anula el nombre del hilo) @Override public void run () (Ball ball = Ball.getBall (); while (ball.isInGame () ) (kickBall (pelota);)) kickBall vacío privado (Ball ball) (si (! ball.getSide (). es igual a (getName ())) (ball.kick (getName ());)))

    Ahora cuidemos la pelota. No será sencillo con nosotros, sino memorable: para que pueda decir quién lo golpeó, de qué lado y cuántas veces. Para hacer esto, usamos un mutex: recopilará información sobre el trabajo de cada uno de los subprocesos; esto permitirá que los subprocesos aislados se comuniquen entre sí. Después del golpe 15, sacaremos la pelota del juego, para no lastimarla gravemente.

    Bola de clase pública (patadas int privadas = 0; instancia de Bola estática privada = nueva Bola (); lado de cadena privada = ""; Bola privada () () Bola estática getBall () (instancia de retorno;) patada vacía sincronizada (String nombre de jugador) (patadas ++; lado = nombre de jugador; System.out.println (patadas + "" + lado);) String getSide () (return side;) boolean isInGame () (return (patadas< 15); } }

    Y ahora dos hilos de jugadores están entrando en escena. Llamémoslos, sin más preámbulos, Ping y Pong:

    Clase pública PingPongGame (PingPongThread player1 = new PingPongThread ("Ping"); PingPongThread player2 = new PingPongThread ("Pong"); Ball ball; PingPongGame () (ball = Ball.getBall ();) void startGame () lanza InterruptedException (player1 .start (); player2.start ();))

    "Estadio lleno de gente - es hora de comenzar el partido". Anunciaremos oficialmente la apertura de la reunión, en la clase principal de la aplicación:

    PingPong de clase pública (public static void main (String args) lanza InterruptedException (PingPongGame game = new PingPongGame (); game.startGame ();))

    Como puede ver, aquí no hay nada furioso. Esto es solo una introducción al subproceso múltiple por ahora, pero ya sabe cómo funciona y puede experimentar: limite la duración del juego no por el número de golpes, sino por el tiempo, por ejemplo. Volveremos al tema del multihilo más adelante; veremos el paquete java.util.concurrent, la biblioteca Akka y el mecanismo volátil. Hablemos también sobre la implementación de subprocesos múltiples en Python.

    Arcilla Breshears

    Introducción

    Los métodos de implementación de subprocesos múltiples de Intel incluyen cuatro fases principales: análisis, diseño e implementación, depuración y ajuste del rendimiento. Este es el enfoque utilizado para crear una aplicación multiproceso a partir de código secuencial. El trabajo con software durante la primera, tercera y cuarta etapas se cubre de manera bastante amplia, mientras que la información sobre la implementación del segundo paso es claramente insuficiente.

    Se han publicado muchos libros sobre algoritmos paralelos y computación paralela. Sin embargo, estas publicaciones cubren principalmente el paso de mensajes, los sistemas de memoria distribuida o los modelos teóricos de computación paralela que a veces son inaplicables a las plataformas multinúcleo reales. Si está listo para tomarse en serio la programación de subprocesos múltiples, probablemente necesitará conocimientos sobre el desarrollo de algoritmos para estos modelos. Por supuesto, el uso de estos modelos es bastante limitado, por lo que muchos desarrolladores de software pueden tener que implementarlos en la práctica.

    No es exagerado decir que el desarrollo de aplicaciones multiproceso es, en primer lugar, una actividad creativa y solo luego una actividad científica. En este artículo, aprenderá acerca de ocho reglas sencillas que lo ayudarán a expandir su base de prácticas de programación concurrente y mejorará la eficiencia del subproceso de sus aplicaciones.

    Regla 1. Seleccione las operaciones realizadas en el código del programa independientemente unas de otras.

    El procesamiento paralelo se aplica solo a aquellas operaciones en código secuencial que se realizan de forma independiente entre sí. Un buen ejemplo de cómo las acciones independientes conducen a un único resultado real es la construcción de una casa. Involucra a trabajadores de muchas especialidades: carpinteros, electricistas, yeseros, fontaneros, techadores, pintores, albañiles, jardineros, etc. Por supuesto, algunos de ellos no pueden comenzar a trabajar antes de que otros hayan terminado sus actividades (por ejemplo, los techadores no comenzarán a trabajar hasta que las paredes estén construidas y los pintores no pintarán estas paredes si no están enlucidas). Pero, en general, podemos decir que todas las personas involucradas en la construcción actúan de forma independiente unas de otras.

    Considere otro ejemplo: el ciclo de trabajo de una tienda de alquiler de DVD que recibe pedidos de determinadas películas. Los pedidos se distribuyen entre los empleados del punto que buscan estas películas en el almacén. Naturalmente, si uno de los trabajadores saca un disco del almacén en el que se grabó una película con Audrey Hepburn, esto de ninguna manera afectará a otro trabajador que busque otra película de acción con Arnold Schwarzenegger, y más aún no afectará a su colega. quien está en busca de discos con nueva temporada de la serie "Friends". En nuestro ejemplo, creemos que todos los problemas asociados con la falta de películas en stock se resolvieron antes de que los pedidos llegaran al punto de alquiler, y el embalaje y envío de cualquier pedido no afectará el procesamiento de otros.

    En su trabajo, probablemente encontrará cálculos que solo se pueden procesar en una secuencia específica, y no en paralelo, ya que las diferentes iteraciones o pasos del ciclo dependen entre sí y deben realizarse en un orden estricto. Tomemos un ejemplo vivo de la naturaleza. Imagina una cierva preñada. Dado que tener un feto dura un promedio de ocho meses, entonces, digan lo que digan, un cervatillo no aparecerá en un mes, incluso si ocho renos quedan preñados al mismo tiempo. Sin embargo, ocho renos al mismo tiempo harían su trabajo a la perfección si se engancharan a todos ellos en el trineo de Santa.

    Regla 2. Aplicar paralelismo con un nivel de detalle bajo

    Hay dos enfoques para la partición paralela de código de programa secuencial: de abajo hacia arriba y de arriba hacia abajo. En primer lugar, en la etapa de análisis de código, se determinan los segmentos de código (los denominados "puntos calientes"), que ocupan una parte importante del tiempo de ejecución del programa. La separación de estos segmentos de código en paralelo (si es posible) proporcionará la máxima ganancia de rendimiento.

    El enfoque ascendente implementa el procesamiento multiproceso de puntos calientes de código. Si no es posible dividir los puntos encontrados en paralelo, debe examinar la pila de llamadas de la aplicación para determinar otros segmentos que estén disponibles para la división en paralelo y que demoren mucho en completarse. Digamos que está trabajando en una aplicación para comprimir gráficos. La compresión se puede implementar utilizando varios flujos paralelos independientes que procesan segmentos individuales de la imagen. Sin embargo, incluso si ha logrado implementar "hotspots" de subprocesos múltiples, no descuide el análisis de la pila de llamadas, como resultado de lo cual puede encontrar segmentos disponibles para la división en paralelo en un nivel superior del código del programa. De esta forma, puede aumentar la granularidad del procesamiento paralelo.

    En el enfoque de arriba hacia abajo, se analiza el trabajo del código del programa y se resaltan sus segmentos individuales, cuya ejecución conduce a la finalización de toda la tarea. Si no hay una independencia clara de los segmentos de código principales, analice sus partes constituyentes para encontrar cálculos independientes. Al analizar el código del programa, puede determinar los módulos de código que consumen más tiempo de CPU. Veamos cómo implementar subprocesos en una aplicación de codificación de video. El procesamiento en paralelo se puede implementar en el nivel más bajo, para píxeles independientes de un fotograma, o en un nivel superior, para grupos de fotogramas que se pueden procesar independientemente de otros grupos. Si se está construyendo una aplicación para procesar varios archivos de video al mismo tiempo, la división en paralelo en este nivel puede ser aún más fácil y el detalle será el más bajo.

    La granularidad de la computación paralela se refiere a la cantidad de computación que se debe realizar antes de sincronizar entre subprocesos. En otras palabras, cuanto menos frecuente sea la sincronización, menor será la granularidad. Los cálculos de subprocesos detallados pueden hacer que la sobrecarga del sistema de subprocesos supere los cálculos útiles realizados por esos subprocesos. El aumento en el número de subprocesos con la misma cantidad de cálculo complica el proceso de procesamiento. El subproceso múltiple de baja granularidad introduce menos latencia del sistema y tiene más potencial de escalabilidad, lo que se puede lograr con subprocesos adicionales. Para implementar el procesamiento paralelo de baja granularidad, se recomienda utilizar un enfoque de arriba hacia abajo y un subproceso en un nivel alto en la pila de llamadas.

    Regla 3. Cree escalabilidad en su código para mejorar el rendimiento a medida que aumenta el número de núcleos.

    No hace mucho tiempo, además de los procesadores de doble núcleo, aparecieron en el mercado los de cuatro núcleos. Además, Intel ya ha anunciado un procesador con 80 núcleos, capaz de realizar un billón de operaciones de punto flotante por segundo. Dado que la cantidad de núcleos en los procesadores solo aumentará con el tiempo, su código debe tener un potencial adecuado de escalabilidad. La escalabilidad es un parámetro mediante el cual se puede juzgar la capacidad de una aplicación para responder adecuadamente a cambios como un aumento en los recursos del sistema (número de núcleos, tamaño de la memoria, frecuencia del bus, etc.) o un aumento en la cantidad de datos. Con el aumento de la cantidad de núcleos en los procesadores futuros, escriba código escalable que aumentará el rendimiento al aumentar los recursos del sistema.

    Parafraseando una de las leyes de C. Northecote Parkinson, podemos decir que "el procesamiento de datos ocupa todos los recursos del sistema disponibles". Esto significa que a medida que aumentan los recursos informáticos (por ejemplo, el número de núcleos), es más probable que todos se utilicen para procesar datos. Regresemos a la aplicación de compresión de video discutida anteriormente. Es poco probable que la adición de núcleos adicionales al procesador afecte el tamaño de los fotogramas procesados; en cambio, el número de subprocesos que procesan el fotograma aumentará, lo que provocará una disminución en el número de píxeles por flujo. Como resultado, debido a la organización de flujos adicionales, la cantidad de datos de servicio aumentará y el grado de granularidad de paralelismo disminuirá. Otro escenario más probable es un aumento en el tamaño o la cantidad de archivos de video que deben codificarse. En este caso, la organización de transmisiones adicionales que procesarán archivos de video más grandes (o adicionales) permitirá que todo el volumen de trabajo se divida directamente en la etapa donde tuvo lugar el aumento. A su vez, una aplicación con tales capacidades tendrá un alto potencial de escalabilidad.

    El diseño e implementación del procesamiento paralelo mediante la descomposición de datos proporciona una mayor escalabilidad en comparación con el uso de la descomposición funcional. El número de funciones independientes en el código del programa suele ser limitado y no cambia durante la ejecución de la aplicación. Dado que a cada función independiente se le asigna un subproceso separado (y, en consecuencia, un núcleo de procesador), con un aumento en el número de núcleos, los subprocesos organizados adicionalmente no causarán un aumento en el rendimiento. Por lo tanto, los modelos de partición en paralelo con descomposición de datos proporcionarán un mayor potencial de escalabilidad de la aplicación debido al hecho de que la cantidad de datos procesados ​​aumentará con la cantidad de núcleos de procesador.

    Incluso si el código del programa está subprocesando funciones independientes, es posible utilizar subprocesos adicionales que se inician cuando aumenta la carga de entrada. Volvamos al ejemplo de construcción de viviendas que se mencionó anteriormente. El propósito peculiar de la construcción es completar un número limitado de tareas independientes. Sin embargo, si se le indica que construya el doble de pisos, probablemente desee contratar trabajadores adicionales en algunas especialidades (pintores, techadores, plomeros, etc.). En consecuencia, necesita desarrollar aplicaciones que puedan adaptarse a la descomposición de datos resultante de una mayor carga de trabajo. Si su código implementa la descomposición funcional, considere la posibilidad de organizar subprocesos adicionales a medida que aumenta el número de núcleos de procesador.

    Regla 4. Utilice bibliotecas seguras para subprocesos

    Si es posible que necesite una biblioteca para manejar puntos calientes de datos en su código, asegúrese de considerar el uso de funciones listas para usar en lugar de su propio código. En resumen, no intente reinventar la rueda desarrollando segmentos de código cuyas funciones ya están provistas en procedimientos optimizados de la biblioteca. Muchas bibliotecas, incluida Intel® Math Kernel Library (Intel® MKL) y Intel® Integrated Performance Primitives (Intel® IPP), ya contienen funcionalidad multiproceso optimizada para procesadores multinúcleo.

    Vale la pena señalar que cuando se utilizan procedimientos de las bibliotecas multiproceso, debe asegurarse de que llamar a una u otra biblioteca no afectará el funcionamiento normal de los subprocesos. Es decir, si las llamadas a procedimientos se realizan desde dos subprocesos diferentes, se deben devolver resultados correctos de cada llamada. Si los procedimientos hacen referencia a las variables de la biblioteca compartida y las actualizan, puede producirse una carrera de datos, lo que afectará negativamente a la fiabilidad de los resultados del cálculo. Para trabajar correctamente con subprocesos, el procedimiento de la biblioteca se agrega como nuevo (es decir, no actualiza nada más que las variables locales) o se sincroniza para proteger el acceso a los recursos compartidos. Conclusión: antes de usar cualquier biblioteca de terceros en el código de su programa, lea la documentación adjunta para asegurarse de que funciona correctamente con las transmisiones.

    Regla 5. Utilice un modelo de subprocesos múltiples adecuado

    Suponga que las funciones de las bibliotecas multiproceso claramente no son suficientes para la división en paralelo de todos los segmentos de código adecuados, y tiene que pensar en la organización de los subprocesos. No se apresure a crear su propia (engorrosa) estructura de subprocesos si la biblioteca OpenMP ya contiene toda la funcionalidad que necesita.

    La desventaja del subproceso múltiple explícito es la imposibilidad de un control preciso del hilo.

    Si solo necesita una separación paralela de bucles que consumen muchos recursos, o la flexibilidad adicional que brindan los subprocesos explícitos es secundaria para usted, entonces, en este caso, no tiene sentido hacer un trabajo adicional. Cuanto más compleja sea la implementación del multiproceso, mayor será la probabilidad de errores en el código y más difícil será su posterior revisión.

    La biblioteca OpenMP se centra en la descomposición de datos y es especialmente adecuada para bucles de subprocesos que trabajan con grandes cantidades de información. A pesar de que solo la descomposición de datos es aplicable a algunas aplicaciones, es necesario tener en cuenta requisitos adicionales (por ejemplo, del empleador o cliente), según los cuales el uso de OpenMP es inaceptable y queda por implementar multiproceso utilizando explícitamente métodos. En este caso, OpenMP se puede utilizar para subprocesos preliminares para estimar las ganancias de rendimiento potenciales, la escalabilidad y el esfuerzo aproximado que se requeriría para posteriormente dividir el código mediante subprocesos múltiples explícitos.

    Regla 6. El resultado del código del programa no debe depender de la secuencia de ejecución de subprocesos paralelos.

    Para el código de programa secuencial, basta con definir una expresión que se ejecutará después de cualquier otra expresión. En el código de subprocesos múltiples, el orden de ejecución de los subprocesos no está definido y depende de las instrucciones del programador del sistema operativo. Estrictamente hablando, es casi imposible predecir la secuencia de subprocesos que se lanzarán para realizar una operación, o determinar qué subproceso lanzará el programador en un momento posterior. La predicción se usa principalmente para reducir la latencia de una aplicación, especialmente cuando se ejecuta en una plataforma con un procesador con menos núcleos que la cantidad de subprocesos organizados. Si un subproceso está bloqueado porque necesita acceso a un área que no está escrita en la caché, o porque necesita ejecutar una solicitud de E / S, el programador lo suspenderá e iniciará el subproceso listo para comenzar.

    Las situaciones de carrera de datos son un resultado inmediato de la incertidumbre en la programación de ejecución de subprocesos. Puede ser incorrecto suponer que algún hilo cambiará el valor de una variable compartida antes de que otro hilo lea ese valor. Con buena suerte, el orden de ejecución de los subprocesos para una plataforma en particular seguirá siendo el mismo en todos los lanzamientos de la aplicación. Sin embargo, los cambios más pequeños en el estado del sistema (por ejemplo, la ubicación de los datos en el disco duro, la velocidad de la memoria o incluso una desviación de la frecuencia nominal de la red de suministro de energía de CA) pueden provocar un orden diferente de ejecución de hilos. Por lo tanto, para el código de programa que funciona correctamente solo con una determinada secuencia de subprocesos, es probable que haya problemas asociados con situaciones de "carrera de datos" y puntos muertos.

    Desde el punto de vista de la mejora del rendimiento, es preferible no restringir el orden de ejecución de los hilos. Se permite una secuencia estricta de ejecución de flujos solo en caso de emergencia, determinada por un criterio predeterminado. En el caso de tal circunstancia, los hilos se lanzarán en el orden especificado por los mecanismos de sincronización proporcionados. Por ejemplo, imagina a dos amigos leyendo un periódico extendidos sobre una mesa. Primero, pueden leer a diferentes velocidades y, segundo, pueden leer diferentes artículos. Y aquí no importa quién lea primero la extensión del periódico; en cualquier caso, tendrá que esperar a su amigo antes de pasar la página. Al mismo tiempo, no hay restricciones en el tiempo y el orden de lectura de los artículos: los amigos leen a cualquier velocidad y la sincronización entre ellos se produce inmediatamente al pasar la página.

    Regla 7. Utilice almacenamiento de flujo local. Asignar bloqueos a áreas de datos específicas según sea necesario

    La sincronización aumenta inevitablemente la carga en el sistema, lo que de ninguna manera acelera el proceso de obtención de los resultados de los cálculos paralelos, pero asegura su corrección. Sí, la sincronización es necesaria, pero no debe abusarse. Para minimizar la sincronización, se utiliza el almacenamiento local de flujos o áreas de memoria asignadas (por ejemplo, elementos de matriz marcados con identificadores de los flujos correspondientes).

    La necesidad de compartir variables temporales por diferentes hilos es rara. Dichas variables deben declararse o asignarse localmente a cada hilo. Las variables cuyos valores son resultados intermedios de la ejecución de subprocesos también deben declararse locales a los subprocesos correspondientes. Se requiere sincronización para sumar estos resultados intermedios en un área de memoria compartida. Para minimizar el estrés potencial en el sistema, es preferible actualizar esta área común lo menos posible. Para los métodos de subprocesos múltiples explícitos, existen API de almacenamiento local de subprocesos que garantizan la integridad de los datos locales desde el inicio de la ejecución de un segmento de código de subprocesos múltiples hasta el inicio del siguiente segmento (o durante el procesamiento de una llamada a una función de subprocesos múltiples hasta el siguiente ejecución de la misma función).

    Si no es posible almacenar transmisiones localmente, el acceso a los recursos compartidos se sincroniza mediante varios objetos, como bloqueos. En este caso, es importante asignar correctamente bloqueos a bloques de datos específicos, lo que es más fácil de hacer si el número de bloqueos es igual al número de bloques de datos. Un único mecanismo de bloqueo que sincroniza el acceso a múltiples áreas de la memoria se usa solo cuando todas estas áreas están constantemente en la misma sección crítica del código del programa.

    ¿Qué hacer si necesita sincronizar el acceso a una gran cantidad de datos, por ejemplo, a una matriz de 10,000 elementos? Proporcionar un solo bloqueo para toda la matriz es definitivamente un cuello de botella en la aplicación. ¿Realmente tiene que organizar el bloqueo para cada elemento por separado? Luego, incluso si 32 o 64 subprocesos paralelos accederán a los datos, tendrá que evitar conflictos de acceso a un área de memoria bastante grande, y la probabilidad de tales conflictos es del 1%. Afortunadamente, existe una especie de medio dorado, los llamados "bloqueos de módulo". Si se utilizan N bloqueos de módulo, cada uno sincronizará el acceso a la N-ésima parte del área de datos compartidos. Por ejemplo, si dos de estos bloqueos están organizados, uno de ellos evitará el acceso a los elementos pares de la matriz y el otro, a los impares. En este caso, los subprocesos, haciendo referencia al elemento requerido, determinan su paridad y establecen el bloqueo apropiado. El módulo de número de bloqueos se selecciona teniendo en cuenta el número de subprocesos y la probabilidad de acceso simultáneo de varios subprocesos a la misma área de memoria.

    Tenga en cuenta que no se permite el uso simultáneo de varios mecanismos de bloqueo para sincronizar el acceso a un área de memoria. Recordemos la ley de Segal: “Una persona que tiene un reloj sabe con certeza qué hora es. Una persona que tiene algunos relojes no está segura de nada ". Supongamos que dos bloqueos diferentes controlan el acceso a una variable. En este caso, el primer bloqueo puede ser utilizado por un segmento del código y el segundo por otro segmento. Entonces, los subprocesos que ejecutan estos segmentos se encontrarán en una situación de carrera por los datos compartidos a los que están accediendo al mismo tiempo.

    Regla 8. Cambie el algoritmo del software si es necesario para implementar subprocesos múltiples

    El criterio para evaluar el desempeño de las aplicaciones, tanto secuenciales como paralelas, es el tiempo de ejecución. Como estimación del algoritmo, es adecuado un orden asintótico. Esta métrica teórica casi siempre es útil para evaluar el rendimiento de una aplicación. Es decir, en igualdad de condiciones, una aplicación con una tasa de crecimiento de O (n log n) (clasificación rápida) se ejecutará más rápido que una aplicación con una tasa de crecimiento de O (n2) (clasificación selectiva), aunque los resultados de estos las aplicaciones son las mismas.

    Cuanto mejor sea el orden de ejecución asintótico, más rápido se ejecutará la aplicación paralela. Sin embargo, incluso el algoritmo secuencial más eficiente no siempre se puede dividir en flujos paralelos. Si el punto caliente de un programa es demasiado difícil de dividir y no hay forma de multiproceso en un nivel más alto de la pila de llamadas del punto caliente, primero debe considerar usar un algoritmo secuencial diferente que sea más fácil de dividir que el original. Por supuesto, hay otras formas de preparar su código para el subproceso.

    Como ilustración del último enunciado, considere la multiplicación de dos matrices cuadradas. El algoritmo de Strassen tiene uno de los mejores órdenes de ejecución asintóticos: O (n2.81), que es mucho mejor que el orden O (n3) del algoritmo ordinario de ciclo triple anidado. Según el algoritmo de Strassen, cada matriz se divide en cuatro submatrices, después de lo cual se realizan siete llamadas recursivas para multiplicar n / 2 × n / 2 submatrices. Para paralelizar llamadas recursivas, puede crear un nuevo hilo que realizará secuencialmente siete multiplicaciones independientes de submatrices hasta que alcancen el tamaño especificado. En este caso, el número de subprocesos crecerá exponencialmente y la granularidad de los cálculos realizados por cada subproceso recién formado aumentará con la disminución del tamaño de las submatrices. Consideremos otra opción: organizar un grupo de siete subprocesos que trabajen simultáneamente y realizar una multiplicación de submatrices. Tras la terminación del grupo de subprocesos, el método Strassen se llama de forma recursiva para multiplicar las submatrices (como en la versión secuencial del código del programa). Si el sistema que ejecuta dicho programa tiene más de ocho núcleos de procesador, algunos de ellos estarán inactivos.

    El algoritmo de multiplicación de matrices es mucho más fácil de paralelizar utilizando un ciclo ternario anidado. En este caso, se aplica la descomposición de datos, en la que las matrices se dividen en filas, columnas o submatrices, y cada uno de los subprocesos realiza ciertos cálculos. La implementación de dicho algoritmo se lleva a cabo utilizando pragmas OpenMP insertados en algún nivel del bucle, o organizando explícitamente los hilos que realizan la división matricial. La implementación de este algoritmo secuencial más simple requerirá muchas menos modificaciones en el código del programa, en comparación con la implementación del algoritmo Strassen multiproceso.

    Entonces, ahora conoce ocho reglas simples para convertir efectivamente código secuencial en paralelo. Si sigue estas pautas, podrá crear soluciones multiproceso significativamente más rápido, con mayor confiabilidad, rendimiento óptimo y menos cuellos de botella.

    Para volver a la página web de tutoriales de programación multiproceso, vaya a