concurrente.pdf

41
1. INTRODUCCIÓN Casi todos los moderno sistema operativo o entorno de programación proporciona soporte para programación concurrente. El mecanismo más popular para esto es alguna provisión para permitir que a múltiples ligero "hilos" dentro de un espacio de dirección única, utilizados desde dentro de un solo programa. Programación con hilos introduce nuevas dificultades incluso para programadores experimentados. Programación concurrente tiene técnicas y errores que no se producen en la programación secuencial. Muchas de las técnicas son evidentes, pero algunas son obvias sólo en retrospectiva. Algunos de los escollos son cómodas (por ejemplo, estancamiento es una especie de insecto agradable — detiene su programa con toda la evidencia intacta), pero algunos toman la forma de sanciones rendimiento insidioso. El propósito de este papel es para darle una introducción a las técnicas de programación que funcionan bien con hilos y para advertirle sobre técnicas o interacciones que funcionan mal. Debe proporcionar el experimentado programador secuencial con suficientes pistas para poder crear un programa de multi‐threaded substancial que trabaja — correctamente, eficiente y con un mínimo de sorpresas. Este documento es una revisión de uno que he publicado originalmente en 1989 [2]. Con los años ese papel se ha utilizado extensivamente en enseñar a los estudiantes cómo programar con hilos. Pero mucho ha cambiado desde hace 14 años, tanto en diseño del lenguaje y el diseño de hardware de computadora. Espero que esta revisión, mientras que presenta esencialmente las mismas ideas que el documento anterior, les hará más accesible y más útil para un público contemporáneo. Un "hilo" es un concepto sencillo: un único flujo secuencial de control. En un lenguaje high‐ level normalmente programar un hilo usando llamadas de procedimiento o método, donde las llamadas seguir la disciplina tradicional pila. Dentro de un único subproceso, hay en todo momento un único punto de ejecución. El programador necesita aprender nada nuevo para usar un único subproceso. Tener "varios subprocesos" en un medio de programa que en cualquier momento el programa tiene múltiples puntos de ejecución, uno en cada uno de sus hilos. El programador puede ver sobre todo los hilos como ejecutar simultáneamente, como si la computadora fueron dotada con tantos procesadores como hay hilos. El programador es necesario para decidir cuando y donde para crear múltiples hilos, o a aceptar tales decisiones hecho para él por los implementadores de paquetes de bibliotecas existentes o sistemas de tiempo de ejecución. Además, el programador de vez en cuando debe ser consciente de que en la computadora no puede de hecho ejecutar todos sus hilos simultáneamente. Tener que los hilos ejecutan dentro de un "espacio de dirección única" significa que el direccionamiento de computación está configurado para permitir las roscas a leer y escribir las mismas localizaciones de memoria. En un lenguaje tradicional high‐level, esto corresponde generalmente al hecho de que las variables (globales) off‐stack son compartidas entre todos los subprocesos del programa. En un lenguaje object‐oriented como C# o Java, las variables estáticas de una clase son compartidas entre todos los hilos, como son las variables de instancia de cualquier objeto que los hilos comparten.* Cada subproceso ejecuta en una pila de llamadas independiente con sus propias variables locales independientes. El programador es * Hay un mecanismo en C# (y en Java) para hacer estática campos thread‐specific y no comparte, pero voy a ignorar esa característica en este papel. 2 . Introducción a la programación con C# hilos

description

Paradigma de programación concurrente

Transcript of concurrente.pdf

Page 1: concurrente.pdf

1. INTRODUCCIÓN

Casi todos los moderno sistema operativo o entorno de programación proporciona soporte para

programación concurrente. El mecanismo más popular para esto es alguna provisión para permitir

que a múltiples ligero "hilos" dentro de un espacio de dirección única, utilizados desde dentro de

un solo programa. Programación con hilos introduce nuevas dificultades incluso para programadores

experimentados. Programación concurrente tiene técnicas y errores que no se producen en la

programación secuencial. Muchas de las técnicas son evidentes, pero algunas son obvias sólo en

retrospectiva. Algunos de los escollos son cómodas (por ejemplo, estancamiento es una especie de

insecto agradable — detiene su programa con toda la evidencia intacta), pero algunos toman la

forma de sanciones rendimiento insidioso. El propósito de este papel es para darle una introducción a las técnicas de programación que

funcionan bien con hilos y para advertirle sobre técnicas o interacciones que funcionan mal. Debe

proporcionar el experimentado programador secuencial con suficientes pistas para poder crear un

programa de multi‐threaded substancial que trabaja — correctamente, eficiente y con un mínimo de

sorpresas. Este documento es una revisión de uno que he publicado originalmente en 1989 [2]. Con los

años ese papel se ha utilizado extensivamente en enseñar a los estudiantes cómo programar con

hilos. Pero mucho ha cambiado desde hace 14 años, tanto en diseño del lenguaje y el diseño de

hardware de computadora. Espero que esta revisión, mientras que presenta esencialmente las

mismas ideas que el documento anterior, les hará más accesible y más útil para un público

contemporáneo. Un "hilo" es un concepto sencillo: un único flujo secuencial de control. En un lenguaje high‐

level normalmente programar un hilo usando llamadas de procedimiento o método, donde las

llamadas seguir la disciplina tradicional pila. Dentro de un único subproceso, hay en todo momento

un único punto de ejecución. El programador necesita aprender nada nuevo para usar un único

subproceso. Tener "varios subprocesos" en un medio de programa que en cualquier momento el programa

tiene múltiples puntos de ejecución, uno en cada uno de sus hilos. El programador puede ver sobre

todo los hilos como ejecutar simultáneamente, como si la computadora fueron dotada con tantos

procesadores como hay hilos. El programador es necesario para decidir cuando y donde para crear

múltiples hilos, o a aceptar tales decisiones hecho para él por los implementadores de paquetes de

bibliotecas existentes o sistemas de tiempo de ejecución. Además, el programador de vez en cuando

debe ser consciente de que en la computadora no puede de hecho ejecutar todos sus hilos

simultáneamente. Tener que los hilos ejecutan dentro de un "espacio de dirección única" significa que el

direccionamiento de computación está configurado para permitir las roscas a leer y escribir las

mismas localizaciones de memoria. En un lenguaje tradicional high‐level, esto corresponde

generalmente al hecho de que las variables (globales) off‐stack son compartidas entre todos los

subprocesos del programa. En un lenguaje object‐oriented como C# o Java, las variables estáticas de

una clase son compartidas entre todos los hilos, como son las variables de instancia de cualquier

objeto que los hilos comparten.* Cada subproceso ejecuta en una pila de llamadas independiente

con sus propias variables locales independientes. El programador es * Hay un mecanismo en C# (y en Java) para hacer estática campos thread‐specific y no comparte, pero voy

a ignorar esa característica en este papel. 2 . Introducción a la programación con C# hilos

Page 2: concurrente.pdf

responsable de utilizar los mecanismos de sincronización de la instalación del hilo de rosca

para asegurarse de que la memoria compartida es accesible de una manera que le dará la respuesta

correcta.*

Page 3: concurrente.pdf

* ElCLR(Common Language Runtime) utilizado por C# Aplicaciones introduce el concepto adicional de

"Dominio de aplicación", que permite múltiples programas para ejecutar en un espacio de direcciones de

hardware único, pero que no afecta a cómo su programa utiliza subprocesos. Instalaciones de hilo siempre se anuncian como "ligero". Esto significa que primitivas de

sincronización, existencia, destrucción y creación de hilo son lo suficientemente baratos para que el programador los utilizará para todas las necesidades de su concurrencia.

Por favor tenga en cuenta que te presento con una colección de técnicas selectiva, sesgada e idiosincrásica. Selectiva, porque una encuesta exhaustiva sería demasiado agotador para servir como una introducción — discutirán sólo las primitivas más importantes de hilo, omitiendo las

características tales como per‐thread información de contexto o el acceso a otros mecanismos como

exclusiones mutuas del núcleo NT o eventos. Parcial, porque presento ejemplos, problemas y

soluciones en el contexto de un conjunto particular de opciones de cómo diseñar una instalación de

hilos — las decisiones tomadas en la programación de C# lenguaje y su sistema compatible de

tiempo de ejecución. Idiosincrásicos, porque las técnicas presentadas aquí derivan de mi

experiencia personal de programación con hilos en los últimos veinticinco años (desde 1978) — no

he intentado representar a colegas que tengan opiniones diferentes sobre los cuales las técnicas de

programación son "buenas" o "importante". Sin embargo, creo que la comprensión de las ideas

presentadas aquí servirá como una base sólida para la programación con hilos concurrentes. A lo largo del papel uso ejemplos escritos en C# [14]. Estos deben ser fácilmente comprensibles

por cualquiera que esté familiarizado con los idiomas modernos object‐oriented, incluyendo Java

[7]. Donde Java difiere significativamente de C#, intento señalar esto. Los ejemplos sirven para

ilustrar puntos de sincronización y concurrencia — no trate de utilizar estos algoritmos reales en

programas reales. Hilos de rosca no son una herramienta para la descomposición paralela automática, donde un

compilador tomará un programa visiblemente secuencial y generar el código objeto para utilizar

varios procesadores. Es un arte completamente diferente, no se que voy a comentar aquí.

2. ¿POR QUÉ USAR CONCURRENCIA?

La vida sería más simple si no necesitas usar concurrencia. Pero hay una gran variedad de fuerzas

que empujan hacia su uso. La más evidente es el uso de multi‐processors. Con estas máquinas, hay

varios puntos simultáneos de ejecución y roscas son una herramienta atractiva para permitir que un

programa aprovechar el hardware disponible. La alternativa, con los sistemas operativos más

convencionales, es para configurar su programa como múltiples procesos separados, en espacios de

direcciones separadas. Esto tiende a ser costoso configurar, y los costos de comunicación entre

espacios de direcciones a menudo son altos, incluso en presencia de segmentos compartidos.

Mediante el uso de una instalación de multi‐threading ligero, el programador puede utilizar los

procesadores barato. Esto parece que funciona bien en sistemas teniendo unos 10 procesadores, en

lugar de 1000 procesadores. Introducción a la programación con C# hilos . 3

Page 4: concurrente.pdf

Es una segunda área donde hilos son útiles en la conducción lentos dispositivos tales como

discos, redes, terminales e impresoras. En estos casos un programa eficiente debería estar haciendo

algún otro trabajo útil mientras se espera para que el dispositivo producir su próximo evento tales

como (la realización de una transferencia de disco) o la recepción de un paquete de la red. Como

veremos más adelante, esto puede ser programado fácilmente con hilos adoptando una actitud que

dispositivo peticiones son todos secuenciales (es decir, suspensión la ejecución del subproceso

invocando hasta que se complete la solicitud), y que el programa mientras tanto otros trabajos en

otros subprocesos. Exactamente las mismas observaciones se aplican a mayor niveles lento las

peticiones, tales como realizar una llamada RPC a un servidor de red. Una tercera fuente de concurrencia es usuarios humanos. Cuando el programa está realizando

una tarea larga para el usuario, el programa todavía deberían responder: deben pintar ventanas

expuestas, barras de desplazamiento deben desplazarse de sus contenidos y cancelar botones deben

clic y aplicar la cancelación. Las roscas son una forma conveniente de programación: la larga tarea

se ejecuta en un subproceso independiente del hilo de procesamiento de eventos entrantes GUI; Si

volver a pintar un dibujo complejo llevará mucho tiempo, tendrá que ser también en un subproceso

independiente. En una sección 6, discutir algunas técnicas para implementar esta. Una fuente final de simultaneidad aparece al construir un sistema distribuido. Aquí nos

encontramos con frecuencia los servidores de red compartida (como un servidor web, una base de

datos o un servidor de impresión de cola), donde el servidor está dispuesto a solicitudes de servicio

de múltiples clientes. Uso de múltiples hilos permite al servidor controlar las solicitudes de los

clientes en paralelo, en lugar de los serializar artificialmente (o crear un proceso de servidor por el

cliente, a un gran coste). A veces puede deliberadamente agregar concurrencia a su programa para reducir la latencia de

las operaciones (el tiempo transcurrido entre llamar a un método y el método devuelve). A

menudo, algunos de los trabajos efectuados por una llamada al método pueden ser diferidas, ya

que no afecta el resultado de la convocatoria. Por ejemplo, al agregar o eliminar algo en un árbol

balanceado podrías felices por volver a la la llamada antes de re‐balancing el árbol. Con hilos puede

lograr esto fácilmente: hacer el re‐balancing en un subproceso independiente. Si el subproceso

independiente está programado a una prioridad más baja, la obra puede hacerse en un momento

cuando está menos ocupado (por ejemplo, cuando se espera de entrada del usuario). Adición de

roscas para diferir el trabajo es una técnica poderosa, incluso en un uni‐processor. Incluso si se hace

el mismo trabajo total, reduciendo la latencia puede mejorar la capacidad de respuesta de su

programa y la felicidad de sus usuarios.

3. EL DISEÑO DE UNA INSTALACIÓN DE HILO

No podemos hablar de cómo programar con hilos hasta que coincidimos en los primitivos

proporcionados por un centro de multi‐threading. Los diversos sistemas que soportan hilos ofrecen

servicios muy similares, pero hay mucha diversidad en los detalles. En general, hay cuatro

mecanismos principales: creación del hilo de rosca, exclusión mutua, esperando acontecimientos y

algún arreglo para conseguir un hilo de un largo plazo no deseados esperar. Para hacer los debates

en este concreto de papel, se basan en la instalación de hilo de C#: el "System.Threading"namespace

además el C# "bloqueo" declaración.4 . Introducción a la programación con C# hilos * Un "delegado" de C# es sólo un objeto construido a partir de un objeto y uno de sus métodos. En Java podría

en cambio explícitamente definir y crear una instancia de una clase adecuada. Cuando miras el "System.Threading"namespace, será (o debe) sentirse intimidado por la gama de

opciones frente a usted:"Monitor"o"Mutex”; “Espera"o"AutoResetEvent";"Interrumpir"o"Abortar¿ "?

Page 5: concurrente.pdf

Afortunadamente, hay una respuesta simple: usar el "cerradura" declaración, el "Monitor" clase y el

"interrumpir" método. Esas son las características que utilizaré para la mayor parte del resto del

documento. Por ahora, usted debe ignorar el resto del "System.Threading", aunque yo a dibujarla

para que la sección 9.

a lo largo del papel, los ejemplos se supone que están dentro del ámbito del " usando Sistema

de ; usandoSystem.Threading;"

3.1. creación de hilo

En C# se crea un subproceso mediante la creación de un objeto de tipo "Hilo de rosca", dando a su

constructor un"ThreadStart"delegar*y el nuevo subproceso "Inicio"método de. El nuevo subproceso

comienza ejecución asincrónica con una invocación de método del delegado. Cuando el método

devuelve, muere el hilo. También puede llamar a la "Únete" método de un hilo: esto hace que el

subproceso de llamada esperar hasta que termine el subproceso dado. Crear y poner en un

subproceso llama a menudo "que se bifurcan". Por ejemplo, el siguiente fragmento de programa ejecuta las llamadas al método

"foo.A()"y"foo.B()"en paralelo y termina sólo cuando han completado las llamadas de método. Por

supuesto, el método "A"bien podría acceder a los campos de"foo".

Hilo t = nuevo hilo (nuevo ThreadStart (foo.A)); t.Start(); foo.B(); t.Join();

En la práctica, probablemente no usarás "Únete"mucho. Más horquilla hilos hilos daimonion

permanente, sin resultados o comunican sus resultados por algún arreglo de sincronización que no

sean de "Unir". Está bien para horquilla un hilo, pero nunca tienen una llamada correspondiente de

"unirse a".

3.2. mutua exclusión

La forma más sencilla que interactúan de hilos es a través del acceso a memoria compartida. En un

lenguaje object‐oriented, esto se expresa generalmente como el acceso a las variables que son los

campos estáticos de una clase, o campos de instancia de un objeto compartido. Desde hilos

funcionan en paralelo, el programador debe organizar explícitamente evitar los errores que se

presentan cuando más de un hilo es acceder a las variables compartidas. La herramienta más

simple para hacer esto es un hombre primitivo que ofrece exclusión mutua (a veces llamada

secciones críticas), especificando para una región particular del código que sólo un subproceso

puede ejecutar allí en cualquier momento. En el diseño de C#, esto se logra con la clase "Monitor"y de

la lengua"bloqueo" declaración:

cerradura Introducción a la programación con C# hilos . (expresión) incrustado-

declaración5

Page 6: concurrente.pdf

El argumento de la " cerradura "declaración puede ser cualquier objeto: en C# cada objeto

inherentemente implementa un bloqueo de exclusión mutua. En cualquier momento, un objeto o

"bloqueado" o "desbloqueado", inicialmente abierto. El "cerradura" declaración bloquea el objeto

indicado, ejecuta las sentencias contenidas y luego abre el objeto. Un hilo de ejecución dentro de la

"cerradura" declaración se dice que "espera" bloqueo del objeto dado. Si otro subproceso intenta

bloquear el objeto cuando ya está cerrada, el segundo subproceso se bloquee (en cola en la

cerradura del objeto) hasta que el objeto está desbloqueado. El uso más común de la " cerradura "declaración es proteger los campos de instancia de un

objeto mediante el bloqueo de ese objeto cada vez que el programa está accediendo a los campos.

Por ejemplo, el siguiente fragmento de programa arregla que sólo un hilo a la vez puede estar

ejecutando el par de instrucciones de asignación en la "SetKV" método.

clase KV { cadena k, v; public voidSetKV (nkstring , string nv) { cerradura (este) { este.k = nk; este.v = nv; } } … }

Sin embargo, existen otros patrones para elegir la cerradura del objeto que protege a las variables

que. En general, que lograr la exclusión mutua en un conjunto de variables asociándolos

(mentalmente) con un objeto determinado. Luego escribes tu programa que accede a las variables

sólo desde un subproceso que tiene cerradura del objeto (es decir, desde un subproceso ejecutando

dentro de un "cerradura" declaración que bloquean el objeto). Esta es la base de la noción de

monitores, descrita por primera vez por Tony Hoare [9]. El lenguaje C# y su ejecución no hacen

restricciones en su elección de qué objeto para bloquear, pero para conservar la cordura debe elegir

obvia. Cuando las variables son campos de instancia de un objeto, ese objeto es la obvia para la

cerradura (al igual que en el "SetKV" método, arriba. Cuando las variables son campos estáticos de

una clase, un objeto conveniente es el proporcionado por el runtime de C# para representar el tipo

de la clase. Por ejemplo, en el siguiente fragmento de la "KV"el campo estático de la

clase"cabeza"está protegido por el objeto"typeof(KV)". El "cerradura" declaración dentro de la

"AddToList" método de instancia proporciona exclusión mutua para agregar un "KV"objeto de la lista

enlazada cuya cabeza es"cabeza": único hilo en un momento puede estar ejecutando las

instrucciones que utilizan "cabeza". En este código al campo de instancia "próximo"también está

protegido por"typeof(KV)".

estática Cabeza de KV = null; KV siguiente = null;

public void AddToList() { lock (typeof(KV)) {System.Diagnostics.Debug.Assert (estepróximo == null); estasiguiente = cabeza; cabeza = este;}} 6 . Introducción a la programación con C# hilos

Page 7: concurrente.pdf

*Esta garantía de atomicidad evita el problema conocido en la literatura como la carrera "wake‐up espera" [18].

†Sin embargo, como veremos en la sección 5.2, es muy difícil no agregar la semántica adicional,

mediante la definición de su propia clase "condición Variable"

3.3. esperando una condición

Puede ver la cerradura de un objeto como un simple mecanismo de programación de recursos. El

recurso está programado es la memoria compartida accedida dentro de la "cerradura" Declaración y

la agenda política es un subproceso en un momento. Pero a menudo el programador necesita

expresar más complicado programar políticas. Esto requiere el uso de un mecanismo que permite

que un subproceso se bloqueará hasta que una condición es true. En hilo sistemas pre‐dating Java,

este mecanismo era generalmente llamado "variables de estado" y correspondió a un objeto

asignado por separado [4,13]. En Java y C# no hay ningún tipo separado para este mecanismo. En

cambio cada objeto inherentemente implementa una variable de condición y el ""clase proporciona

estática"espera","pulso"y"PulseAll" métodos para manipular la variable de condición de un objeto.

público clase sellada Monitor { public static bool Wait(Object obj) {...} public static voidPulse(Object obj) {...} public static voidPulseAll(Object obj) {...}...}

Un subproceso que llama "Espera"ya debe tener cerradura del objeto (de lo contrario, la

llamada"espera"producirá una excepción). El "espera" operación atómico se desbloquea el objeto y

bloquea el subproceso*. Un subproceso que está bloqueado de esta manera se dice estar "esperando

en el objeto". El "pulso" método no hace nada a menos que haya al menos un hilo esperando en el

objeto, en cuyo caso se despierta al menos una tal subproceso en espera (pero posiblemente más de

uno). El "PulseAll"método es como"pulso", salvo que se despierta todos los hilos actualmente

esperando en el objeto. Cuando un subproceso es despertado dentro "espera" después de bloqueo,

re‐locks el objeto, entonces devuelve. Tenga en cuenta que la cerradura del objeto puede no estén

disponible inmediatamente, en cuyo caso el hilo recién despertado se bloqueará hasta que el

bloqueo está disponible. Si un subproceso llama a "Espera"cuando adquirió la cerradura del objeto varias veces,

el"Espera"método comunicados (y más adelante re‐acquires) el bloqueo que el número de veces. Es importante ser consciente de que el hilo recién despertado podría no ser el siguiente

subproceso a adquirir el bloqueo: algún otro subproceso puede intervenir. Esto significa que podría

cambiar el estado de las variables protegido por el bloqueo entre la llamada de ""y el hilo de"esperar". Esto tiene consecuencias que analizaré en la sección 4.6.

En sistemas pre‐dating Java, el "Espera"procedimiento o método tomó dos argumentos: una

cerradura y una variable de condición; en Java y C#, éstos se combinan en un único argumento, que

es al mismo tiempo la cerradura y la cola de espera. En cuanto a los sistemas anteriores, esto

significa que el "Monitor" clase admite solamente una variable de condición por cerradura†. Introducción a la programación con C# hilos . 7

Page 8: concurrente.pdf

Bloqueo del objeto protege los datos compartidos que se utilizan para la decisión de

programación. Si un hilo A quiere el recurso, se bloquea el objeto apropiado y examina los datos

compartidos. Si el recurso está disponible, sigue el hilo. Si no, se desbloquea el objeto y bloques,

llamando "espera". Más tarde, cuando algún otro hilo B pone a disposición de los recursos que

despierta el hilo A llamando "pulso"o"PulseAll". Por ejemplo, podríamos añadir lo siguiente

"GetFromList"método de la clase"KV". Este método espera hasta que la lista enlazada es non‐empty y

luego elimina el elemento superior de la lista.

pública estática KV GetFromList() {KV res; cerradura(typeof(KV)) { mientras (cabeza == null) Monitor.Wait (typeof(KV)); res = cabeza; cabeza = res.next; res.next = null;para la limpieza} volver res; }

y el siguiente código para el "AddToList"el método podría ser utilizado por un hilo para agregar un

objeto a"cabeza"y despierta un hilo que estaba esperando lo

public void AddToList() { lock (typeof(KV)) {/ * estamos asumiendo estepróximo == null * / estesiguiente = cabeza; cabeza = esta; Monitor.Pulse (typeof(KV)); } }

3.4. interrumpir un hilo

La parte final de la instalación del hilo de rosca que voy a discutir es un mecanismo para

interrumpir un subproceso concreto, causando que se espera hacia atrás por un largo plazo. En el

sistema de ejecución de C# esto es proporcionado por el hilo "interrumpir" método:

público clase sellada Hilo { public void Interrupt() {...}...}

Si un subproceso "t"está bloqueado esperando a un objeto (es decir, está bloqueado en una llamada

de"Monitor.Wait") y otro hilo llamadas"t.Interrupt()", luego"t"se reanudará la ejecución por re‐locking

del objeto (después de esperar por la cerradura a ser desbloqueado, si es necesario) y luego

tirando"ThreadInterruptedException". (Lo mismo es cierto si el hilo se llama "Thread.Sleep"o"t.Join".)

Alternativamente, si "t" no espera un objeto (y no está durmiendo o esperando adentro "t.Join"),

entonces el hecho de 8 . Introducción a la programación con C# hilos

Page 9: concurrente.pdf

que "Interrumpir"ha sido llamado es registrado y a tirar del hilo"ThreadInterruptedException"la próxima

vez esperas o duerme. Por ejemplo, considere un hilo "t"eso se llamaKVde "GetFromList"método y se bloquea esperando

unKVobjeto a estar disponibles en la lista enlazada. Parece atractivo que si algún otro hilo del

cómputo decide el "GetFromList" llamada ya no es interesante (por ejemplo, el usuario hace clic en

Cancelar con su ratón), luego "t"debe devolver de"GetFromList". Si el manejo de hilos la Cancelar

solicitud pasa saber el objeto en el que "t"está esperando, y luego sólo podría poner una bandera y

llamada"Monitor.Pulse" en ese objeto. Sin embargo, mucho más a menudo la real llamada

"Monitor.Wait" se oculta bajo varias capas de abstracción, completamente invisible para el hilo que se

encarga de la Cancelar solicitar. En esta situación, el manejo de hilos la Cancelar petición puede

alcanzar su objetivo llamando "t.Interrupt()". Por supuesto, en algún lugar de la pila de llamadas de

""debería haber un controlador para"ThreadInterruptedException". Exactamente lo que debes

hacer con la excepción depende de su semántica deseada. Por ejemplo, podríamos arreglar eso una llamada interrumpida del "GetFromList"retorna"null":

pública estática KV GetFromList() {KV res = null; probar{ lock (typeof(KV)) { mientras (cabeza == null) Monitor.Wait (typeof(KV)); res = cabeza; cabeza = head.next; res.next = null; }} atrapar(ThreadInterruptedException) {} volver res; }

Las interrupciones son complicadas, y su uso produce programas complicados. Los analizaremos

más detalladamente en la sección 7.

4. USO DE CERRADURAS: ACCESO COMPARTIDO DATOS

La regla básica para el uso de exclusión mutua es sencilla: en un programa de multi‐threaded

mutables compartidos todos los datos deben estar protegidos por asociándola con cerradura de

algún objeto, y usted deberá acceder a los datos sólo de un hilo que sostiene ese bloqueo (es decir,

desde un subproceso ejecutando dentro de un "cerradura" declaración que bloquean el objeto).

4.1. protección de datos

El más simple error relacionado con las cerraduras se produce cuando fallas proteger algunos datos

mutables y entonces puede acceder a él sin los beneficios de la sincronización. Por ejemplo,

considere el siguiente fragmento de código. El campo "tabla"representa una tabla que puede ser

llenada con los valores del objeto llamando"Insertar". El "Insertar"método funciona insertando un

objeto non‐null en el índice de"yo"de"mesa", entonces incrementar"". La tabla está inicialmente vacía

(todos "null").Introducción a la programación con C# hilos . 9

Page 10: concurrente.pdf

clase de tabla {Tabla de objetos [] = nuevo objeto [1000]; int i = 0;

public void Insert (Object obj) { si (obj! = null) {(1) — tabla [i] = obj; (2) — i ++; } }

… } clase de tabla

Ahora considerar qué pasaría si llama hilo A "Insert(x)"simultáneamente con hilo B

llamando"Insert(y)". Si el orden de ejecución resulta ser que rosca A ejecuta (1), hilo B ejecuta (1) y

luego A hilo ejecuta (2), luego hilo B ejecuta (2), provocará la confusión. En lugar del efecto deseado

(que "x"y"y"se insertan en"mesa", en los índices separados), el estado final que sería "y" está

correctamente en la tabla, pero "x" se ha perdido. Además, desde el (2) ha sido ejecutado dos veces,

un vacío (nulo) ranura ha quedado huérfanos en la tabla. Estos errores podrían haberlo evitados

adjuntando (1) y (2) en un "cerradura" declaración, de la siguiente manera.

public void Insert (Object obj) { si (obj! = null) { cerradura(este) {(1) — tabla [i] = obj; (2) — i

++; } } }

El " cerradura "declaración aplica la serialización de las acciones de los hilos, para que un

subproceso ejecuta las sentencias dentro de la" cerradura "declaración, entonces el otro subproceso

ejecuta. Los efectos de sincronización acceso a datos mutables pueden ser extraños, ya que dependerán

de la relación de sincronización exacta entre tu ropa. En la mayoría de los ambientes esta relación

de sincronización es non‐deterministic (debido a real‐time efectos tales como errores de página o el

uso de instalaciones real‐time temporizador) o asincronía real en un sistema de multi‐processor. En

un multi‐processor los efectos pueden ser especialmente difíciles de predecir y comprender, porque

ellos dependen de los detalles de la consistencia de memoria de la computadora y algoritmos de

caché. Sería posible diseñar un lenguaje que le permite asociar explícitamente variables con cerraduras

particulares y luego le impide acceder a las variables a menos que el hilo tiene la cerradura

adecuada. Pero C# (y la mayoría de otros idiomas) no proporciona ninguna ayuda para esto: usted

puede elegir cualquier objeto alguno como el bloqueo de un determinado conjunto de variables.

Una manera alternativa para evitar el acceso no sincronizada es utilizar las herramientas de análisis

estático o dinámico. Por ejemplo, hay 10 . Introducción a la programación con C# hilos

Page 11: concurrente.pdf

Herramientas experimentales [19] ese cheque en tiempo de ejecución que las cerraduras se

llevan a cabo durante el acceso a cada variable, y que advierten si se utiliza un conjunto

inconsistente de cerraduras (o sin cerradura en absoluto). Si usted tiene este tipo de herramientas

disponible, considerar seriamente usándolos. Si no, entonces usted necesita programador

considerable disciplina y uso cuidadoso de búsqueda y herramientas de navegación. Acceso fuera

de sincronización, o mal sincronizado, se convierte cada vez más probable como la granularidad

del bloqueo se convierte más fino y sus reglas de bloqueo se correspondientemente más complejos.

Tales problemas surgirán con menos frecuencia si utilizas muy simple, grueso grano, bloqueo. Por

ejemplo, utilizar la cerradura de la instancia del objeto a proteger todos los campos de instancia de

una clase y utilizar "typeof(clase)" para proteger los campos estáticos. Por desgracia, bloqueo de

grano muy grueso puede causar otros problemas, que se describe a continuación. Así que el mejor

consejo es hacer el uso de bolsas de ser tan simple como sea posible, pero no más simple. Si usted es

tentado a utilizar arreglos más elaborados, estar completamente seguros que los beneficios valen los

riesgos, no sólo que el programa luce mejor.

4.2. invariantes

Cuando los datos protegidos por una cerradura están complicados en absoluto, muchos

programadores parece conveniente pensar en la cerradura como la protección de los invariantes de

los datos asociados. Una invariante es una función booleana de los datos que ocurre cuando la

cerradura asociada no se lleva a cabo. Así que cualquier subproceso que adquiere la cerradura sabe

que empieza con la verdadera invariante. Cada hilo tiene la responsabilidad de restaurar la

invariante antes de soltar el bloqueo. Esto incluye la restauración de la invariante antes de llamar

"espera", puesto que también libera el bloqueo. Por ejemplo, en el fragmento de código por encima (para insertar un elemento en una tabla), la

invariante es que "yo"es el índice de la primera" null "elemento"mesa"y todos los elementos más allá

de índice"yo"son" null ". Tenga en cuenta que las variables mencionadas en la invariante son

accesibles sólo mientras "este" está cerrada. Tenga en cuenta también que no es cierto la invariante

después de la primera instrucción de asignación, pero antes que la segunda — está garantizado sólo

cuando el objeto está desbloqueado. Con frecuencia los invariantes son bastante simples que apenas piensa en ellos, pero a menudo

su programa beneficiará de escribirlos explícitamente. Y si son demasiado complicados para anotar,

probablemente estás haciendo algo mal. Tal vez escribas abajo las invariantes de manera informal,

al igual que en el párrafo anterior, o puede usar un lenguaje de especificación formal. A menudo es

sensato tener tu programa comprobar explícitamente sus invariantes. También es generalmente una

buena idea indicar explícitamente, en el programa, que la cerradura protege los campos. Independientemente de cómo formalmente te gusta pensar de invariantes, tienes que ser

consciente del concepto. Liberando la cerradura mientras que las variables están en un estado

incoherente transitorio conducirá inevitablemente a la confusión si es posible que otro subproceso

adquirir la cerradura mientras estás en este estado.

4.3. bloquea que involucra sólo las cerraduras

En algunos sistemas de hilo [4] su programa será deadlock si un subproceso intenta bloquear un

objeto que ya está cerrada. C# (y Java) explícitamente permiten que un subproceso bloquear un

objeto varias veces de manera anidada: el sistema runtime realiza un seguimiento de qué

Page 12: concurrente.pdf

subproceso ha bloqueado el objeto y con qué frecuencia. El objeto permanece bloqueado Introducción a la programación con C# hilos . 11

Page 13: concurrente.pdf

(y por lo tanto está bloqueado el acceso simultáneo por otros subprocesos) hasta que el hilo haya

destrabado el objeto el mismo número de veces. Esta función 'bloqueo de re‐entrant' es una conveniencia para el programador: desde dentro un

" cerradura "declaración puede llamar a otro de sus métodos que también bloquea el mismo objeto,

sin riesgo de bloqueo. Sin embargo, la característica es double‐edged: si se llama al método otro en

un momento cuando las invariantes monitor no son verdaderas, entonces el otro método será

probable que se portan mal. En sistemas que prohíben re‐entrant bloqueo tal conducta es

prevenida, siendo reemplazado por un callejón sin salida. Como he dicho antes, estancamiento

suele ser un bicho más agradable que devolver la respuesta equivocada. Existen numerosos casos más elaborados de bloqueo que involucra sólo las cerraduras, por

ejemplo:

cerraduras de hilo A objeción M1; cerraduras de hilo B objeción M2; bloques de hilo A intentar bloquear M2; hilo bloques B tratando de cerradura M1.

La regla más eficaz para evitar tales bloqueos es tener un orden parcial para la adquisición de

bloqueos en su programa. En otras palabras, hacer que para cualquier par de objetos {M1, M2}, cada

hilo que necesite tener M1 y M2 cerrada al mismo tiempo lo hace mediante el bloqueo de los objetos

en el mismo orden (por ejemplo, M1 siempre está cerrada antes de M2). Esta regla evita totalmente

los interbloqueos que involucra sólo las cerraduras (aunque como veremos más adelante, hay otros

posibles bloqueos cuando su programa usa el "Monitor.Wait" método). Hay una técnica que a veces resulta más fácil lograr este orden parcial. En el ejemplo anterior,

hilo A probablemente no estaba tratando de modificar exactamente el mismo conjunto de datos

como hilo B. con frecuencia, si examina cuidadosamente el algoritmo puede particionar los datos en

trozos más pequeños protegidos por separado cerraduras. Por ejemplo, cuando hilo B intentó

cerradura M1, podría en realidad quiero acceso datos separados de los datos que estaba accediendo

A hilo bajo M1. En tal caso podría proteger estos datos inconexos bloqueando un objeto separado,

M3 y evitar el estancamiento. Tenga en cuenta que esto es sólo una técnica para permitirle tener un

orden parcial en las cerraduras (M1 antes M2 antes de M3, en este ejemplo). Pero recuerde que

cuanto más te dedicas a esta pista, la más complicada que su fijación se convierte, y más probable es

que se confunda acerca de que la cerradura está protegiendo los datos y terminan con algún

sincronizado acceso a datos compartidos. (¿mencioné que tener su estancamiento programa casi

siempre es un riesgo preferible a tener su programa dar la respuesta equivocada?)

4.4. bajo rendimiento a través de los conflictos de la cerradura

Suponiendo que haya dispuesto su programa para tener suficientes candados que todos los datos

están protegidos y una lo suficientemente fina granularidad que no deadlock, los restantes

problemas bloqueo preocuparse son todos los problemas de rendimiento. Cuando un subproceso tiene una cerradura, potencialmente se detiene otro subproceso de

progreso — si los otros bloques de hilo tratando de adquirir el bloqueo. Si el primer subproceso

puede utilizar los recursos de la máquina, está bien. Pero si 12 . Introducción a la programación con C#

hilos

Page 14: concurrente.pdf

el primer subproceso, manteniendo el bloqueo, deja de avanzar (por ejemplo mediante el

bloqueo en la otra cerradura, o tomando un fallo de página o esperando a un dispositivo de

entrada-salida), luego se degrada el rendimiento total de su programa. El problema es peor en un

multi‐processor, donde no hay un único hilo puede utilizar toda la máquina; aquí si usted causa

otro subproceso se bloquee, podría significar que un procesador pasa inactivo. En general, para

obtener buen rendimiento se deben organizar que Trabe los conflictos son eventos raros. Es la mejor

manera de reducir los conflictos de bloqueo bloquear en una granularidad más fina; Pero esto

presenta complejidad y aumenta el riesgo de no sincronizado acceso a datos. No hay forma de este

dilema — es un trade‐off inherentes en computación concurrente. El ejemplo más típico donde el bloqueo de granularidad es importante en una clase que

gestiona un conjunto de objetos, por ejemplo un conjunto de abrir archivos tamponados. La

estrategia más simple es usar una sola cerradura global para todas las operaciones: abrir, cerrar,

leer, escribir y así sucesivamente. Pero esto impediría varias escrituras en archivos separados

proceder en paralelo, por ninguna razón. ¿Una estrategia mejor es utilizar una cerradura para

operaciones en la lista mundial de archivos abiertos, y un candado por abrir el archivo para las

operaciones que afectan sólo a ese archivo. Afortunadamente, ésta también es la manera más obvia

de utilizar las cerraduras en una lengua object‐oriented: el bloqueo global protege las estructuras de

datos globales de la clase, y bloqueo de cada objeto se utiliza para proteger los datos específicos de

esa instancia. El código puede parecer algo como lo siguiente.

clase F {cabeza estática F = null; / / protegido por typeof(F) string minombre; / / inmutable F siguiente = null; / / protegido por datos D typeof(F); / / protegido por "esto"

pública estática F Abierto (string nombre) { lock (typeof(F)) { para (F f = cabeza; f! = null; f = f.next) {si (nombre.Equals(f.myname)) volver f; } / / Si obtiene un nuevo F, enqueue en "cabeza" y devolverlo. volver ...; } }

public void Escritura (F f, string msg) { cerradura (este) {/ / acceso "f.data"}}

}

Allí es una importante sutileza en el ejemplo anterior. La forma que elegí implementar la lista

global de archivos era pasar una lista vinculada a través de la "siguiente" campo de instancia. Esto

dio como resultado un ejemplo donde parte de los datos de instancia debe Una introducción a la

programación con C# hilos . 13

* Recordar que la prioridad del hilo de rosca es no un mecanismo de sincronización: un hilo de alta prioridad

puede conseguir fácilmente superado por un hilo de prioridad más bajo, por ejemplo si los hilos de alta

prioridad choca con un fallo de página.

estar protegido por el bloqueo global y la parte por el percerradura de instancia ‐object. Este es sólo

uno de una amplia variedad de situaciones donde usted puede optar por proteger a diferentes

campos de un objeto con diversas cerraduras, para conseguir mayor eficiencia accediendo

simultáneamente desde diferentes subprocesos. Por desgracia, este uso tiene algunas de las mismas características de sincronización acceso a

datos. La corrección del programa se basa en la capacidad de acceder a diferentes partes de la

memoria del ordenador simultáneamente desde diferentes subprocesos, sin interferir mutuamente

los accesos. El modelo de memoria Java especifica que esto funcionará correctamente mientras las

diversas cerraduras protegen diferentes variables (por ejemplo, campos de instancia diferente). Sin

embargo, la especificación de lenguaje C#, es actualmente silenciosa sobre este tema, así que usted

debe programar conservador. Recomiendo que usted asume accesos para referencias a objetos y

Page 15: concurrente.pdf

valores escalares de 32 bits o más (por ejemplo, "int"o"flotador") puede continuar

independientemente bajo diversas cerraduras, pero que tiene acceso a valores más pequeños (como

"bool") tal vez no. Y no sería más prudente acceder a distintos elementos de una matriz de valores

pequeños tales como "bool" bajo diversas cerraduras.

Allí es una interacción entre bloqueos y el planificador de hilo que puede producir problemas de

rendimiento particularmente insidiosa. El programador es la parte de la implementación del hilo de

rosca (a menudo parte del sistema operativo) que decide cuál de los non‐blocked hilos en realidad

deberían darse un procesador para correr en. Generalmente el programador hace su decisión

basándose en una prioridad asociada a cada subproceso. (C# le permite ajustar la prioridad de un

hilo mediante la asignación de la rosca "Prioridad"propiedad*.) Cerradura conflictos pueden llevar a

una situación donde algunos hilos de alta prioridad nunca avanza, a pesar de que su prioridad alta

indica que es más urgente que los threads ejecutándose en realidad. Esto puede suceder, por ejemplo, en el siguiente escenario en un uni‐processor. Hilo A es de

alta prioridad, hilo B es prioridad media y rosca C es una prioridad baja. La secuencia de eventos

es:

C se está ejecutando (por ejemplo, porque están bloqueados en alguna parte A y B); C las cerraduras objeto M; B despierta y descarta C (es decir, B funciona en vez de C puesto que B tiene mayor prioridad); B se embarca en un cálculo muy larga; A se despierta y descarta B (ya que tiene mayor prioridad); Un intenta bloquear M, pero no puede porque aún está bloqueado por C; Por bloques, así que el procesador es devuelto a B; B continúa su cómputo muy largo.

Efecto la red es que un hilo de alta prioridad (A) es incapaz de progresar a pesar de que el

procesador está siendo utilizado por un hilo de prioridad media (B). Este estado es 14 . Introducción a

la programación con C# hilos

Page 16: concurrente.pdf

estable hasta que no haya tiempo de procesador disponible para el hilo de baja prioridad C

completar su trabajo y desbloquear M. Este problema se conoce como "inversión prioritaria". El programador puede evitar este problema arreglando para que C elevar su prioridad antes de

bloquear M. Pero esto puede ser bastante inconveniente, ya que se trata de considerar para cada

cerradura que otras prioridades del hilo pueden estar involucrados. La mejor solución a este

problema se encuentra en programador de subproceso del sistema operativo. Idealmente, debería

plantear artificialmente prioridad C mientras que eso es necesario para permitir A progresar con el

tiempo. El programador de Windows NT no hace esto, pero arregla que hilos incluso baja prioridad

avanzar, sólo a un ritmo más lento. C eventualmente completará su trabajo y A hacer progresos.

4.5. liberando el bloqueo dentro de un "cerradura" declaración

Hay veces cuando usted quiere desbloquear el objeto en algunas regiones del programa anidado

dentro de un " cerradura "declaración. Por ejemplo, deberías desbloquear el objeto antes de llamar

una abstracción de nivel inferior que se bloquean o ejecutar durante mucho tiempo (para evitar

provocar retrasos en otros subprocesos que desea bloquear el objeto). C# (pero no Java) ofrece para

este uso ofreciendo las operaciones de crudo "Enter(m)"y"Exit(m)" como métodos estáticos de la

"Monitor" clase. Usted debe ejercer cuidado extra si te aprovechas de esto. En primer lugar, usted

debe estar seguro de que las operaciones están correctamente entre corchetes, incluso en presencia

de excepciones. En segundo lugar, debe estar preparado para el hecho de que podría haber

cambiado el estado de los datos del monitor cuando tuvieron el objeto desbloqueado. Esto puede

ser difícil si usted llama "salida" explícitamente (en lugar de solo poner fin a la "cerradura"

declaración) en un lugar donde fueron encajadas en un control de flujo construir como una cláusula

condicional. El contador de programa ahora puede depender el estado previo de los datos del

monitor, implícitamente tomando una decisión que ya no puede ser válida. Así desanimo a este

paradigma, para reducir la tendencia a presentar errores muy sutiles. Algunos hilos sistemas, aunque no C#, permiten un otro uso de llamadas separadas del

"Enter(m)"y"Exit(m)", en las cercanías de bifurcar. Podrías estar ejecutando con un objeto bloqueado y

quiero un nuevo hilo para seguir trabajando en los datos protegidos, mientras que el hilo original

continúa sin mayor acceso a los datos de la bifurcación. En otras palabras, desea transferir la

celebración de la cerradura a la rosca recién bifurcada, atómicamente. Esto puede lograr mediante

el bloqueo del objeto con "Enter(m)" en vez de un ""Declaración y más tarde llamada"Exit(m)" en el hilo de tracción. Esta táctica es muy peligrosa, es difícil de verificar el correcto funcionamiento del monitor. Te recomiendo que no lo haces incluso en sistemas que le permiten (a diferencia de C#).

4.6. sin bloqueo de programación

Como hemos visto, utilizando las cerraduras es un arte delicado. Adquirir y liberar bloqueos

ralentiza su programa, y algunos usos de cerraduras inadecuados pueden producir rendimiento

espectacularmente grandes sanciones, o incluso muerto. A veces los programadores responden a

estos problemas tratando de escribir programas concurrentes que son correctos sin necesidad de

utilizar las cerraduras. Esta práctica es general llamado "programación de lock‐free". Requiere

aprovechando la atomicidad de ciertas operaciones primitivas, o usando a lower‐level primitivas

tales como instrucciones de barrera de memoria. Introducción a la programación con C# hilos . 15

Page 17: concurrente.pdf

* La especificación del lenguaje Java tiene un modelo de memoria razonablemente precisa [7, capítulo 17], que

dice, aproximadamente, que se acceden atómicamente referencias a objetos y cantidades escalares no mayores

a 32 bits. C# todavía no tiene un modelo de memoria definida con precisión, que hace lock‐free programación

aún más arriesgado. Con máquina moderna arquitecturas y compiladores modernos, esto es algo sumamente

peligroso. Los compiladores son libres de re‐order acciones dentro de la semántica formal

especificada del lenguaje de programación y la voluntad a menudo hacen. Esto lo hacen por

razones sencillas, como mover Código fuera de un bucle, o para los más sutiles, como optimizar el

uso de la memoria caché de un procesador on‐chip o tomando ventaja de los ciclos de la máquina lo

contrario ociosa. Además, las arquitecturas de máquina multi‐processor tienen increíblemente

complejas reglas para cuando los datos se mueven entre cachés de procesador y la memoria

principal, y cómo esto se sincroniza entre los procesadores. (Incluso si alguna vez un procesador no

hace referencia a una variable, la variable podría ser en la memoria caché del procesador, si la

variable es en la misma línea de caché que alguna otra variable que el procesador hizo referencia.) Es, sin duda, posible escribir programas lock‐free correcto, y hay muchas investigaciones

actuales sobre cómo configurar este [10]. Pero es muy difícil y es muy poco probable que usted

conseguirá lo correcto si no utilizas técnicas formales para verificar esa parte de su programa.

También tienes que estar muy familiarizado con el modelo de memoria de su lenguaje de

programación.[15]* Mirar en tu programa y conjeturar (o discutiendo informalmente) que es

correcto es probable que resulte en un programa que funciona correctamente casi todo el tiempo,

pero muy ocasionalmente misteriosamente obtiene la respuesta equivocada. Si estás siendo tentado a escribir código lock‐free a pesar de estas advertencias, por favor

primero considere cuidadosamente si es preciso su creencia de que las cerraduras son demasiado

caras. En la práctica, muy pocas piezas de un programa o sistema son realmente críticas para su

desempeño. Sería triste reemplazar un programa correctamente sincronizado con un lock‐free de

almost‐correct uno y luego descubrir que aun cuando su programa no se rompe, su rendimiento no

es significativamente mejor. Si usted busca la web para algoritmos de sincronización lock‐free, la que más frecuentemente se

hace referencia se llama "Algoritmo de Peterson" [16]. Hay varias descripciones de él en los sitios

web de la Universidad, acompañados a menudo informales "pruebas" de su corrección. En

dependencia de éstos, los programadores han intentado mediante este algoritmo en el código

práctica, sólo para descubrir que su programa ha desarrollado las condiciones de carrera sutil. La

resolución de esta paradoja se encuentra en la observación de que las "pruebas" dependen,

generalmente implícitamente, presunciones sobre la atomicidad de las operaciones de memoria.

Algoritmo de Peterson publicado se basa en un modelo de memoria conocido como "consistencia

secuencial" [12]. Por desgracia, no multi‐processor moderna proporciona consistencia secuencial,

porque la pena de rendimiento en su memoria sub‐system sería demasiado grande. No lo hagas. Otra técnica de lock‐free que la gente trata a menudo se llama "bloqueo double‐check". La intención

es inicializar una variable compartida poco antes de su primer uso, de tal manera que la variable

puede accederse posteriormente sin necesidad de utilizar un "cerradura" declaración. Por ejemplo,

considere el siguiente código. 16 . Introducción a la programación con C# hilos * Esta discusión es bastante diferente de la correspondiente en la versión de 1989 de este documento [2]. La

diferencia de velocidad entre los procesadores y su memoria principal ha aumentado tanto que ha impactado

los problemas de diseño high‐level a escribir programas concurrentes. Técnicas de programación que

anteriormente eran correctas se han convertido en incorrectas.

Foo theFoo = null;

público Foo GetTheFoo() { si (theFoo == null) { cerradura (este) { si (theFoo == null) theFoo = new Foo();}} volver theFoo; }

Page 18: concurrente.pdf

Intención del programador aquí es que la primera rosca para llamar "GetTheFoo"causará el objeto

requerido ser creado y inicializado y todas las llamadas subsiguientes de"GetTheFoo"regresará este

mismo objeto. El código es tentador, y sería correcto con los compiladores y multi‐processors de los

años 80.* Hoy, en la mayoría de los idiomas y en casi todas las máquinas, es incorrecto. Las

deficiencias de este paradigma se han discutido ampliamente en la comunidad de Java [1]. Hay dos

problemas. Primera, si "GetTheFoo"se llama el procesador A y entonces más adelante procesador B, es

posible que procesador B verá el objeto correcto non‐null de referencia para"theFoo", pero va a leer

correctamente almacenados en memoria caché los valores de las variables de instancia dentro

de"theFoo", porque llegaron en caché de B en algún momento anterior, estando en la misma línea de

caché que alguna otra variable que se almacena en caché en B. En segundo lugar, es legítimo que el compilador re‐order declaraciones dentro de un "

cerradura "declaración, si el compilador puede demostrar que no interfieren. Considere lo que

podría ocurrir si el compilador convierte el código de inicialización para "nuevo Foo()" en línea, y

luego re‐orders las cosas para que la asignación a "theFoo"ocurre antes de la inicialización de la

variable instancia de"theFoo". Un hilo cronológico en otro procesador entonces podría ver un non‐

null "theFoo" antes el objeto instancia está correctamente inicializado. Hay muchas maneras que la gente ha intentado arreglar este [1]. Se equivocan. La única manera

que usted puede estar seguro de que este código funcione es obvia, dónde te envuelves todo en un

"cerradura" declaración:

Foo theFoo = null;

público Foo GetTheFoo() { cerradura (este) { si (theFoo == null) theFoo = new Foo(); volver theFoo; }} Introducción a la programación con C# hilos . 17

*Las variables condiciones descritas aquí no son las mismas que las descritas originalmente por

Hoare [9].Diseño de Hoare hecho proporcionaría una garantía suficiente para hacer este re‐

testing redundante. Pero el diseño dado aquí parece ser preferible, ya que permite una

implementación mucho más simple, y el cheque adicional no es generalmente muy caro.

De hecho, C# hoy es implementado de una manera que hace doble‐check de bloqueo funcione

correctamente. Pero confiando en que esto parece una manera muy peligrosa al programa.

5. UTILIZACIÓN DE ESPERA Y PULSO: PROGRAMACIÓN DE RECURSOS COMPARTIDOS

Cuando desee programar la forma en la que varios subprocesos acceder a un recurso compartido, y

la exclusión mutua one‐at‐a‐time simple de cerraduras no es suficiente, usted querrá hacer su

bloque de hilos en espera de un objeto (el mecanismo llamado "variables de estado" en otros

sistemas del hilo de rosca). Recordar el "GetFromList«método de mi anterior»KV"ejemplo. Si la lista enlazada está vacía,

"GetFromList"bloques hasta"AddToList" genera algunos datos más:

cerradura (typeof(KV)) { mientras (cabeza == null) Monitor.Wait (typeof(KV)); res = cabeza; cabeza = res.next; res.next = null; }

Esto es bastante sencillo, pero todavía hay algunas sutilezas. Observe que cuando un subproceso

regresa de la llamada "espera" su primera acción después de re‐locking el objeto es verificar una vez

Page 19: concurrente.pdf

más si está vacía la lista enlazada. Este es un ejemplo de la siguiente pauta general, que recomiendo

para todo su uso de variables de condición:

mientras (! expresión) Monitor.Wait(obj);

Usted podría creer que re‐testing la expresión es redundante: en el ejemplo anterior,

"AddToList"hecha la lista non‐empty antes de llamar"Pulso". Pero la semántica de "pulso" no

garantizan que el hilo despertado será la próxima para bloquear el objeto. Es posible que algún otro

hilo consumidor intervendrá, bloquear el objeto, quitar el elemento de lista y desbloquear el objeto,

antes de que el hilo recién despertado puede bloquear el objeto.* Un beneficio secundario de esta

regla de programación es que permitiría la aplicación de "Pulso"para despertar (raramente) más de

un hilo; Esto puede simplificar la implementación de "espera", aunque ni Java ni C# en realidad dar

tanta libertad el implementador hilos. Pero la razón principal para defender la utilización de este patrón es hacer su programa más

evidente y más sólidamente, corregir. Con este estilo es inmediatamente evidente que la expresión

es verdadera antes de ejecución los siguientes comandos. Sin él, este hecho podría verificarse

solamente por mirar a todos los lugares que podrían pulso el objeto. En otras palabras, este

Convenio de programación le permite verificar la corrección por la inspección de local, que siempre

es preferible a la inspección global. 18 . Introducción a la programación con C# hilos * El C# runtime incluye una clase para hacer esto, "ReaderWriterLock". Persigo este ejemplo aquí en parte

porque los mismos problemas se presentan en un montón de problemas más complejos y en parte porque la

especificación de "ReaderWriterLock" guarda silencio sobre cómo o si su aplicación aborda los temas que vamos

a discutir. Si te preocupas por estas cuestiones, tal vez encuentre que su propio código funcionará mejor que

"ReaderWriterLock". Una ventaja final de esta Convención es que permite una sencilla programación de llamadas a

"Pulso"o"PulseAll"— wake‐ups extra son benignos. Codificación cuidadosamente para asegurar que

sólo los hilos correctos están despertados ahora es sólo una cuestión de rendimiento, no una

corrección uno (pero por supuesto que debe asegurarse de que al menos se despertó los hilos

correctos).

5.1. usando "PulseAll"

El "Pulso"primitivo es útil si usted sabe que a lo sumo un subproceso puede ser despertado

provechosamente."PulseAll"despierta todas las roscas que han llamado"Espera". Si usted siempre

programar en el estilo recomendado de re‐checking una expresión después de regresar de "espera",

entonces la corrección de su programa será afectada si reemplazas las llamadas de"pulso"con

llamadas de"PulseAll". Un uso de "PulseAll"es cuando quieres simplificar su programa al despertar varios subprocesos,

aunque sabe que no todos pueden progresar. Esto le permite ser menos cuidadosos acerca de

separar espera diferentes razones en diferentes colas de espera hilos. Este uso cotiza un rendimiento

ligeramente más pobre para mayor simplicidad. Otro uso del "PulseAll" es cuando realmente

necesitas despertar varios subprocesos, porque el recurso que acaba de hacer disponible puede ser

utilizado por varios otros subprocesos. Un ejemplo sencillo donde "PulseAll"es útil en la planificación política conocida como exclusiva

compartida de bloqueo (o lectores/escritores de bloqueo). Comúnmente se utiliza cuando tienes

algunos datos compartidos ser leído y escrito por varios subprocesos: su algoritmo será correcto (y

tener un mejor desempeño) si permites que varios subprocesos leer los datos simultáneamente,

Page 20: concurrente.pdf

pero un hilo de modificación de los datos debe hacerlo cuando ningún otro subproceso es acceder a

los datos. Los siguientes métodos de implementación de esta política de programación*. Cualquier hilo de

querer leer sus llamadas de datos "AcquireShared", y luego Lee los datos, entonces se llama

"ReleaseShared". Asimismo cualquier hilo de querer modificar las llamadas de datos

"AcquireExclusive", entonces modifica los datos, entonces se llama "ReleaseExclusive". Cuando la

variable "" es mayor que cero, cuenta el número de lectores activos. Cuando es negativo hay un

escritor activo. Cuando es cero, no hay hilo está utilizando los datos. Si un lector potencial dentro

de "AcquireShared"que se encuentra"" es menor que cero, debe esperar hasta que el escritor llama

"ReleaseExclusive".

clase RW { int i = 0; / / protegido por "esto"

public void AcquireExclusive() { cerradura (este) { mientras (yo! = 0) Monitor.Wait (este); Introducción a la programación con C# hilos . 19

Page 21: concurrente.pdf

Te = -1; } }

public void AcquireShared() { cerradura (este) { mientras (yo < 0) Monitor.Wait (este); i ++;}}

public void ReleaseExclusive() { cerradura (este) {i = 0; Monitor.PulseAll (este); } }

public void ReleaseShared() { cerradura (este) {i--; si(i == 0) Monitor.Pulse (este); } }

} / / clase RW

Usando "PulseAll"es conveniente en"ReleaseExclusive", porque un escritor terminación no necesita

saber cuántos lectores ahora son capaces de proceder. Pero aviso que usted podría re‐code este

ejemplo utilizando sólo "pulso", mediante la adición de un contador de cómo muchos lectores están

esperando y llamando "Pulso" muchas veces "ReleaseExclusive". El "PulseAll" es sólo una

conveniencia, aprovechándose de la información ya disponible para la ejecución de subprocesos.

Observe que no hay ninguna razón para usar "PulseAll"en"ReleaseShared", porque sabemos que a lo

sumo un escritor bloqueado puede avanzar provechosamente. Esta codificación particular de la fijación exclusiva compartida ejemplifica muchos de los

problemas que pueden ocurrir cuando espera en objetos, como veremos en las siguientes secciones.

Como discutimos estos problemas, yo presentaré codificaciones revisadas de este paradigma

bloqueo.

5.2. espurios Estela-ups

Si mantienes el uso del "Espera"muy simple, usted puede introducir la posibilidad de despertar los

subprocesos que no se pueden progresar útil. Esto puede suceder si utilizas "PulseAll"cuando"pulso"

sería suficiente, o si tiene hilos esperando en un solo objeto por múltiples razones diferentes. Por

ejemplo, los métodos de fijación exclusiva compartida arriba tienen lectores y escritores tanto

espera "este". Esto significa que cuando llamamos "PulseAll"en"ReleaseExclusive", el efecto será 20 . Introducción a la programación con C# hilos * Es más difícil en Java, que no proporciona "Monitor.Enter"y"Monitor.Exit". despertar ambas clases de subprocesos bloqueados. Pero si un lector es el primero en cerrar el

objeto, se incrementará "i"y evitar que un escritor potencial despertado de avanzar hasta el lector

más tarde llamadas"ReleaseShared". El costo de esto es tiempo extra que pasó en el programador

del hilo de rosca, que normalmente es un lugar caro para ser. Si tu problema es tal que estos falsos

wake‐ups será común, entonces realmente quieres dos lugares para esperar — uno para los lectores

y otro para los escritores. Sólo necesita llamar a un lector de terminación "pulso" en el objeto donde

esperan escritores; un escritor terminación llamaría "PulseAll" en uno de los objetos, dependiendo de

la era non‐empty. Por desgracia, en C# (y en Java) para cada cerradura sólo podemos esperar en un objeto, la

misma que estamos utilizando como la cerradura. Para programar alrededor de esto tenemos que

usar un segundo objeto y su cerradura. Es sorprendentemente fácil de hacerlo mal, generalmente

mediante la introducción de una carrera donde cierto número de hilos se ha comprometido a

esperar en un objeto, pero no tienen suficiente de una cerradura llevó a cabo para evitar que algún

otro llamado hilo "PulseAll" sobre ese objeto, y así el wake‐up se pierde y los impasses del programa.

Creo que los siguientes "CV"clase, como se utiliza en la siguiente revisión"RW" ejemplo, obtiene esta

bien (y usted debe ser capaz de re‐use este exacta "CV" clase en otras situaciones).*

Page 22: concurrente.pdf

clase CV {objeto m; / / la cerradura asociados con este CV público CV (objeto m) {/ / Constructor cerradura(este) esta.m = m;}

public void Wait() {/ / Pre: este hilo tiene "m" exactamente una vez bool entrar = false; / / usando la bandera de "entrar" da error de limpieza manejo si m no es bloqueado intenta { cerradura (este) {Monitor.Exit(m); entrar = true; Monitor.Wait (este); finalmente {}} { si (entrar)

Monitor.Enter(m);}}

public void Pulse() { bloqueo (esta) Monitor.Pulse (este);}

public void PulseAll() { bloqueo (esta) Monitor.PulseAll (este);}

} / / clase CV una introducción a la programación con C# hilos . 21

Page 23: concurrente.pdf

Ahora podemos revisar "RW"para arreglar que sólo espera los lectores esperen en la

principal"RW"objeto, y que espera escritores esperen el auxiliar"wQueue"objeto. (Inicialización

"wQueue"es un poco complicado, ya que nosotros no podemos hacer referencia" esto "al inicializar

una variable de instancia.)

clase RW { int i = 0; / / protegido por "esta" int readWaiters = 0; / / protegido por "esto" wQueue CV = null;

public void AcquireExclusive() { cerradura (este) { si (wQueue == null) wQueue = nuevo CV (esto) mientras que (yo! = 0) wQueue.Wait(); i = -1;}}

public void AcquireShared() { cerradura (este) {readWaiters ++; mientras(yo < 0) Monitor.Wait (este); readWaiters--; i ++; } }

public void ReleaseExclusive() { cerradura (este) {i = 0; si(readWaiters > 0) {Monitor.PulseAll (este);} más{ si (wQueue! = null) wQueue.Pulse();} } }

public void ReleaseShared() { cerradura (este) {i--; si(i == 0 & & wQueue! = null)

wQueue.Pulse(); } }

} / / clase RW 22 . Introducción a la programación con C# hilos

Page 24: concurrente.pdf

5.3. los conflictos bloqueo espurias

Otra fuente potencial de programación excesiva sobrecarga proviene de situaciones donde se

despierta un hilo de esperar en un objeto y antes de hacer trabajo útil el subproceso se bloquee

tratando de bloquear un objeto. En algunos diseños de hilo, esto es un problema en la mayoría

wake‐ups, porque el hilo despertó inmediatamente tratará de adquirir el bloqueo asociado a la

variable de condición, que se lleva a cabo actualmente por el subproceso haciendo el wake‐up. C#

evita este problema en casos simples: llamando "Monitor.Pulse" en realidad no deja el hilo despierto

empezar a ejecutar. En cambio, se transfiere a una cola de"lista" en el objeto. La cola de lista consta

de hilos que están dispuestos a cerrar el objeto. Cuando un subproceso abre el objeto, como parte de

esa operación tomará un hilo en la cola de listo e iniciarlo ejecutando. Sin embargo todavía hay un conflicto de bloqueo espurias en el "RW"clase. Cuando un escritor

terminación interior "ReleaseExclusive"llamadas"wQueue.Pulse(this)", todavía tiene"este" bloqueado.

En un uni‐processor esto a menudo no sería un problema, pero en un multi‐processor el efecto es

probable que haya que un escritor potencial es despertado dentro "CV.Espera", se ejecuta tanto

como la"finalmente"cuadra y luego intentando bloquear"m"— porque ese bloqueo se mantiene

todavía por el escritor terminación, ejecutando simultáneamente. Unos microsegundos después el

escritor terminación abre el "RW" objeto, permitiendo que el escritor continuar. Esto nos ha costado

dos operaciones re‐schedule adicional, que es un gasto significativo.

Afortunadamente hay es una solución simple. Puesto que el escritor terminación no acceder

a los datos protegidos por el bloqueo después de la llamada "wQueue.Pulse", podemos pasar la

llamada a después del final de la "cerradura" declaración, como sigue. Observe que acceder a ""

todavía está protegido por la cerradura. Una situación similar ocurre en "ReleaseShared".

public void ReleaseExclusive() { bool doPulse = false; cerradura(este) {i = 0; si(readWaiters > 0) {Monitor.PulseAll (este);} más{doPulse = (wQueue! = null);} } wQueue.Pulse() si (doPulse); }

public void ReleaseShared() { bool doPulse = false; cerradura(este) { i--; si(i == 0) doPulse = (wQueue! = null); } Introducción a la programación con C# hilos . 23

Page 25: concurrente.pdf

si wQueue.Pulse() (doPulse);}

Hay situaciones potencialmente aún más complicadas. Cómo obtener el mejor rendimiento es

importante para su programa, debes considerar cuidadosamente si un subproceso recién despierto

necesariamente se bloqueará en algún otro objeto poco después de que empiece a correr. Si es así,

tienes que organizar para aplazar la wake‐up para un momento más adecuado. Afortunadamente,

la mayoría del tiempo en C# la lista cola utilizada por "Monitor.Pulse" hará lo correcto para usted

automáticamente.

5.4. hambre

¿ Cada vez que tiene un programa que toma decisiones de planificación, debes preocuparte sobre

cómo justo estas decisiones son; en otras palabras, son iguales todos los subprocesos o son un poco

más favorecidas? Cuando usted bloquee un objeto, esta consideración se aborda por ti mediante la

implementación de hilos — típicamente por una regla de first‐in‐first‐out para cada nivel de

prioridad. Sobre todo, esto también es válido cuando se utiliza "Monitor.Wait" en un objeto. Pero a

veces el programador debe involucrarse. La forma más extrema de la injusticia es "inanición",

donde algunos hilos voluntad nunca avanzar. Esto puede surgir en nuestro ejemplo bloqueo reader‐

writer (por supuesto). Si el sistema está cargado, así que siempre hay al menos un hilo de querer ser

un lector, el código vigente se morirán de hambre a escritores. Esto podría ocurrir con el siguiente

patrón.

Rosca A llamadas "AcquireShared"; Yo: = 1; Hilo B llamadas "AcquireShared"; Yo: = 2; Rosca A llamadas "ReleaseShared"; Yo: = 1; Hilo C llamadas "AcquireShared"; Yo: = 2; Hilo B llamadas "ReleaseShared"; Yo: = 1; ... etc.

Ya que siempre hay un lector activo, nunca hay un momento cuando un escritor puede proceder;

potenciales escritores siempre permanecerá cerrados, esperando "yo"para reducir a 0. Si la carga es

tal que esto es realmente un problema, tenemos que hacer el código aún más complicado. Por

ejemplo, podemos organizar que un nuevo lector podría aplazar dentro "AcquireShared" si hubo un

escritor potencial bloqueado. Podríamos hacer esto mediante la adición de un contador para los

escritores bloqueados, como sigue.

int writeWaiters = 0; public voidAcquireExclusive() { cerradura (este) { si (wQueue == null) wQueue = nuevo CV (esto);writeWaiters ++; mientras(yo! = 0) wQueue.Wait(); writeWaiters--; Te = -1; }} 24 . Introducción a la programación con C# hilos

Page 26: concurrente.pdf

public void AcquireShared() { cerradura (este) {readWaiters ++; si(writeWaiters > 0) {wQueue.Pulse(); Monitor.Wait(this); } mientras (yo < 0) Monitor.Wait (este); readWaiters--; i ++;

} }

No hay límite a lo complicado esto puede llegar a ser, implementando cada vez más elaborado

políticas de programación. El programador debe actuar con moderación y sólo agregar

funcionalidades si realmente están obligados por la carga real en el recurso.

5.5. complejidad

Como puedes ver, preocuparse por estos falsos wake‐ups, cerradura de conflictos y el hambre hace

que el programa más complicado. La primera solución del problema lector/grabador que le mostré

tenía 16 líneas dentro de los cuerpos de método; la versión final tenía 39 líneas (incluyendo el "CV"

clase) y algunos razonamientos muy sutiles sobre su corrección. Tienes que tener en cuenta, para

cada caso, si el costo potencial de ignorar el problema es suficiente para merecer escribiendo un

programa más complejo. Esta decisión dependerá de las características de rendimiento de su

implementación de hilos, si usted está utilizando un multi‐processor y sobre la carga prevista en su

recurso. En particular, si su recurso es en su mayoría no en uso entonces los efectos de rendimiento

no será un problema, y deberías adoptar el estilo de codificación más simple. Pero a veces son

importantes, y sólo debe ignorarlos después de considerar explícitamente si están obligados en su

situación particular.

5.6. deadlock

Usted puede introducir los interbloqueos esperando sobre objetos, incluso aunque han cuidado de

tener un orden parcial en la adquisición de las cerraduras. Por ejemplo, si tienes dos recursos

(llamada de ellos (1) y (2)), la siguiente secuencia de acciones produce un interbloqueo.

Hilo A adquiere recursos (1); Hilo B adquiere recursos (2); Hilo A quiere (2), así que se llama "Monitor.Wait" para esperar (2); Hilo B quiere (1), así se llama "Monitor.Wait" para esperar (1).

Bloqueos como ésta no son significativamente diferentes de las que hablamos en relación con las

cerraduras. Usted debe arreglar que existe un orden parcial sobre los recursos gestionados con

variables de condición, y que cada hilo deseen Introducción a la programación con C# hilos . 25

Page 27: concurrente.pdf

adquirir múltiples recursos hace según este orden. Así, por ejemplo, puedes decidir que (1) se

ordena antes (2). Luego hilo B no se permitiría a tratar de adquirir (1) mientras sujeta (2), así que no

se produciría el estancamiento. Una interacción entre bloqueos y esperando en los objetos es una fuente sutil de bloqueo.

Considere los siguientes dos métodos (muy simplificados).

clase GG { static objeto un = new Object(); estáticaB objeto = new Object(); static bool lista = false;

public static void Get() { bloqueo (a) { bloqueo (b) { mientras (! listo) Monitor.Wait(b);}}}

public static void Give() { bloqueo (a) { bloqueo (b) {lista = true; Monitor.Pulse(b); } } }

} / / clase GG

Si "listo"es" falso "y del hilo de rosca A las llamadas"Haz", bloqueará la llamada de"Monitor.Wait(b)".

Esto desbloquea "b", pero deja "un" bloqueado. Si el subproceso B llama "da", con la intención de

causar una llamada de"Monitor.Pulse(b)", en lugar de ello bloqueará tratando de bloquear"un", y su

programa habrá un veredicto. Claramente, este ejemplo es trivial, desde la cerradura del "un" hace

no protege los datos (y la posibilidad de interbloqueo sea evidente), pero el general patrón ocurre. Más a menudo este problema ocurre cuando usted adquiere un bloqueo a nivel de una

abstracción de su programa y luego llama a un nivel inferior, que bloquea (desconocido para el

nivel superior). Si este bloque puede ser liberado sólo por un hilo que sostiene la cerradura de nivel

superior, usted será deadlock. Es generalmente arriesgado poner en una abstracción de nivel

inferior manteniendo uno de sus cerraduras, a menos que entiendes plenamente las circunstancias

bajo las cuales podría bloquear el método llamado. Una solución aquí es explícitamente

desbloquear la cerradura nivel superior antes de llamar a la abstracción de nivel inferior, como

hemos hablado anteriormente; Pero como ya comentamos, esta solución tiene sus propios peligros.

Una mejor solución es organizar para poner fin a la "cerradura" declaración antes de llamar a. Puede

encontrar más discusiones sobre este problema, conocido como el "problema de monitor anidados",

en la literatura [8]. 26 . Introducción a la programación con C# hilos

Page 28: concurrente.pdf

6. USANDO HILOS: TRABAJANDO EN PARALELO

Como ya comentamos anteriormente, existen varias clases de situaciones donde usted querrá un

subproceso independiente de la bifurcación: utilizar un multi‐processor; para hacer trabajo útil

mientras esperan un dispositivo lento; para satisfacer a los usuarios humanos trabajando en varias

acciones a la vez; proporcionar servicio de red a múltiples clientes simultáneamente; y aplazar

hasta un tiempo menos ocupado. Es muy común encontrar programas de aplicación sencilla utilizando varios hilos. Por ejemplo,

tal vez tengas un hilo haciendo su cómputo principal, un segundo hilo escribir alguna salida en un

archivo, un tercer hilo esperando (o respondiendo a) entrada de usuario interactiva y un cuarto hilo

ejecutando en segundo plano para limpiar sus estructuras de datos (por ejemplo, re‐balancing un

árbol). También es muy probable que paquetes de bibliotecas que utilizas generará internamente

sus propias roscas. Cuando están programando con hilos, que generalmente los dispositivos lento impulsión a

través de las llamadas sincrónicas biblioteca que suspensión el subproceso de llamada hasta la

acción del dispositivo completa, pero permite que otros subprocesos en su programa para

continuar. Usted no encontrará necesidad de utilizar más viejos esquemas para operación

asincrónica (como rutinas de terminación de entrada-salida). Si no quieres esperar por el resultado

de una interacción del dispositivo, invocarlo en un subproceso independiente. Si quieres tener

simultáneamente múltiples solicitudes de dispositivo excepcional, invocarlas en varios

subprocesos. En general las bibliotecas proporcionadas con el entorno C# proporcionan llamadas

sincrónicas apropiadas para la mayoría de los propósitos. Descubrirás que las bibliotecas legadas

no hagan (por ejemplo, cuando el programa C# es llamar a objetos COM); en esos casos, es

generalmente una buena idea añadir una capa proporciona un paradigma llamado sincrónico, para

que el resto de su programa puede ser escrito en un estilo natural thread‐based.

6.1. usando hilos en Interfaces de usuario

Si su programa está interactuando con un usuario humano, generalmente querrá que sea sensible

incluso mientras se está trabajando en una solicitud. Esto es particularmente cierto de interfaces

window‐oriented. Si su pantalla interactiva va tonto es particularmente irritante para el usuario

(por ejemplo, windows no repintan o las barras de desplazamiento no desplazarse) sólo porque una

consulta de base de datos está tomando mucho tiempo. Usted puede alcanzar respuesta mediante el

uso de hilos extras En Windows Forms de C# la maquinaria de que su programa oye acerca de eventos de la

interfaz de usuario al registrarse delegados como event‐handlers para los diversos controles.

Cuando ocurre un evento, el control llama el event‐handler apropiado. Pero el delegado se llama

síncrono: hasta que vuelve, no hay más eventos serán reportados al programa, y esa parte de la

pantalla del usuario aparecerá congelada. Así que usted debe decidir si la acción solicitada es lo

suficientemente corta para que puedes hacerlo con seguridad sincrónicamente, o si deberías hacer

la obra en un subproceso independiente. Una buena regla general es que si la event‐handler puede

terminar en un período de tiempo que no es importante para un humano (digamos, 30

milisegundos) entonces puede funcionar sincrónicamente. En los demás casos, el controlador de

eventos sólo debe extraer los datos de parámetros apropiados desde el estado de la interfaz de

Page 29: concurrente.pdf

usuario (por ejemplo, el contenido de las cajas de texto o los botones de radio) y solicite un

subproceso asincrónico hacer el trabajo. Introducción a la programación con C# hilos . 27

Page 30: concurrente.pdf

En la fabricación de este juicio llamar necesitas considerar el peor caso retraso que pudiera

incurrir el código. Cuando usted decide mover el trabajo provocada por un evento de la interfaz de usuario en un

subproceso independiente, tienes que tener cuidado. Síncrono, debe capturar una visión consistente

de las partes pertinentes de la interfaz de usuario en el caso de controlador delegado, antes de

transferir el trabajo para el subproceso de trabajo asincrónico. También debes tener cuidado que el

subproceso de trabajo se desista si se convierte en irrelevante (por ejemplo, el usuario hace clic en

"Cancelar"). En algunas aplicaciones debe serializar correctamente para que el trabajo se hace en el

orden correcto. Finalmente, debes tener cuidado en la actualización de la interfaz de usuario con

resultados del trabajador. No es legal que un subproceso arbitrario modificar el estado de la

interfaz de usuario. Por el contrario, debe utilizar el subproceso de trabajo el "Invoke" método de un

control para modificar su estado. Esto es porque los diversos objetos instancia de control no son

thread‐safe: sus métodos no pueden ser llamados simultáneamente. Dos técnicas generales pueden

ser útiles. Uno es mantener exactamente un subproceso de trabajo y organizar sus controladores de

eventos para alimentarla peticiones a través de una cola de ese programa explícitamente. Una

alternativa es crear subprocesos de trabajo según sea necesario, tal vez con números de secuencia en

sus peticiones (generados por sus controladores de eventos). Puede ser difícil cancelar una acción que procede en un subproceso de trabajo asincrónico. En

algunos casos es conveniente utilizar la "Thread.Interrupt" mecanismo (discutido más adelante). En

otros casos es muy difícil hacerlo correctamente. En estos casos, considere poniendo una bandera

para registrar la cancelación, y luego comprobar esa bandera antes de que el subproceso de trabajo

hace algo con sus resultados. Un subproceso de trabajo cancelado luego silenciosamente puede

morir si se ha vuelto irrelevante a los deseos del usuario. En todos los casos de cancelación,

recuerda que no es necesario hacer todos los SANEAMIETNO síncrono con la solicitud de

cancelación. Todo lo que necesita es que después de responder a la solicitud de cancelación, el

usuario nunca verá nada de lo que resulta de la actividad cancelada.

6.2. con hilos en servidores de red

Servidores de red generalmente deben servir a múltiples clientes simultáneamente. Si su red de

comunicación se basa en RPC [3], esto sucederá sin ningún trabajo por su parte, desde el lado del

servidor de su sistema RPC invocará a cada llamada entrante concurrente en un subproceso

independiente, por un número adecuado de hilos internamente para su implementación se

bifurcan. Pero varios subprocesos se puede utilizar incluso con otros paradigmas de comunicación.

Por ejemplo, en un protocolo de connection‐oriented tradicional (por ejemplo, transferencia de

archivos en capas encima de TCP), probablemente debería tenedor un hilo para cada conexión

entrante. Por el contrario, si escribes un programa cliente y no quieres esperar la respuesta de un

servidor de red, invocar el servidor desde un subproceso independiente.

6.3. aplazar trabajo

La técnica de la adición de hilos de rosca con el fin de aplazar el trabajo es muy valiosa. Hay varias

variantes del esquema. El más simple es que tan pronto como su método ha trabajado bastante para

calcular su resultado, horquilla un hilo para hacer el resto de la obra y luego volver a tu

identificador de llamadas en el hilo original. Esto 28 . Introducción a la programación con C# hilos

Page 31: concurrente.pdf

reduce la latencia de la llamada al método (el tiempo transcurrido desde ser llamado a devolver),

con la esperanza de que el trabajo diferido puede hacerse más barato más tarde (por ejemplo,

porque un procesador pasa inactivo). La desventaja de este enfoque más simple es que podría crear

grandes cantidades de hilos, e incurre en el costo de la llamada "horquilla" cada vez. A menudo, es

preferible mantener un hilo de limpieza solo y pide que lo alimentan. Es incluso mejor cuando el

ama de llaves doesn't necesita alguna información de los hilos principales, más allá del hecho de

que hay trabajo por hacer. Por ejemplo, esto será cierto cuando el ama de llaves es responsable de

mantener una estructura de datos en una forma óptima, aunque los hilos principales todavía tendrá

la respuesta correcta sin esta optimización. Una técnica adicional aquí es programar el ama de

llaves para fusionar las peticiones similares en una sola acción, o limitarse a ejecutar no más a

menudo que un intervalo periódico elegido.

6.4. canalización

En un multi‐processor, hay un uso especializado de subprocesos adicionales que es particularmente

valioso. Usted puede construir una cadena de relaciones producer‐consumer, conocido como una

tubería. Por ejemplo, cuando inicia hilo A una acción, todo lo que hace es enqueue una solicitud en

un búfer. Hilo B toma la acción desde el buffer, realiza parte del trabajo, entonces cola en un búfer

de segundo. Hilo C toma a partir de ahí y hace el resto de la obra. Esto forma una tubería de three‐

stage. Los tres hilos funcionan en paralelo, excepto cuando sincroniza para acceder a los buffers, así

que este gasoducto es capaz de utilizar hasta tres procesadores. En su mejor momento, canalización

puede alcanzar casi lineal speed‐up y puede aprovechar un multi‐processor. Un oleoducto también

puede ser útil en un uni‐processor si cada subproceso encontrará algunos retrasos real‐time (tales

como errores de página, manejo del dispositivo o red de comunicaciones). Por ejemplo, el siguiente fragmento de programa utiliza una tubería simple de tres etapas. El

"cola" clase implementa una cola FIFO sencilla, utilizando una lista enlazada. Se inicia una acción en

la tubería mediante una llamada a la "PaintChar" método de una instancia de la "PipelinedRasterizer"

clase. Un subproceso auxiliar se ejecuta en "rasterizador"y otro en"pintor". Estos hilos se comunican a

través de las instancias de la "cola" clase. Tenga en cuenta esa sincronización para

"QueueElem"objetos se consigue mediante la celebración de la correspondiente"cola de" bloqueo del

objeto.

clase QueueElem {/ / sincronizado por cerradura público objeto v; la cola / / inmutable público QueueElem siguiente = null; / / protegido por bloqueo de cola

público QueueElem (objeto v) { este.v = v;}

} / / clase QueueElem Introducción a la programación con C# hilos . 29

Page 32: concurrente.pdf

clase Cola {cabeza QueueElem = null; / / protegido por "esta" cola de QueueElem = null; / /

protegido por "esto"

public void Enqueue (objeto v) {/ / Append "v" a esta cola cerradura (esto) {QueueElem e = new QueueElem(v); si(cabeza == null) {cabeza = e; Monitor.PulseAll(this); } más {tail.next = e;}

cola = e; } }

público Objeto Dequeue() {/ / quitar el primer elemento de res objeto cola = null; cerradura(esto) { mientras (cabeza == null) Monitor.Wait(this); res = head.v; cabeza = head.next;} res de retorno ; }

} / / clase cola

clase PipelinedRasterizer {rasterizeQ cola = new Queue(); Cola paintQ = new Queue(); Hilo de

rosca t1, t2; F fuente; Pantalla d;

public void PaintChar(char c) {rasterizeQ.Enqueue(c)};

vacío Rasterizer() { mientras (true) { char c = (char) (rasterizeQ.Dequeue()); / / convertir caracteres a un mapa de bits... Mapa de bits, b = f.Render(c); paintQ.Enqueue(b); }} 30 .

Introducción a la programación con C# hilos

Page 33: concurrente.pdf

vacío Painter() { mientras (true) {Bitmap b = (Bitmap)(paintQ.Dequeue()); / / pintura de mapa de

bits en el dispositivo de gráficos... d.PaintBitmap(b);}}

público PipelinedRasterizer (Font f, pantalla d) {this.f = f; this.d = d; t1 = nuevo hilo (nuevo ThreadStart (esto.Rasterizador)); T1.Start(); T2 = nuevo hilo (nuevo ThreadStart (esto.Pintor));

T2.Start(); }} / / clase PipelinedRasterizer

Hay dos problemas con canalización. Primero, tienes que ser cuidadoso acerca de cuánto del trabajo

obtiene en cada etapa. Lo ideal es que las etapas son iguales: Esto proporcionará el máximo

rendimiento, utilizando plenamente todos sus procesadores. Lograr este ideal requiere mano

tuning y re‐tuning como los cambios en el programa. En segundo lugar, el número de etapas en su

tubería estáticamente determina la cantidad de concurrencia. Si sabes cómo muchos procesadores, y

exactamente donde se producen las demoras real‐time, esto va a estar bien. Para ambientes más

flexibles o portátiles puede ser un problema. A pesar de estos problemas, la segmentación es una

técnica poderosa que tiene aplicabilidad amplia.

6.5. el impacto de su entorno

El diseño de su sistema operativo y bibliotecas de ejecución afectará la medida que es deseable o

útil a las roscas de la horquilla. Las bibliotecas que se utilizan comúnmente con C# son

razonablemente thread‐friendly. Por ejemplo, incluyen entrada sincrónica y métodos de producción

que suspensión sólo el subproceso de la llamada, no todo el programa. La mayoría de las clases de

objeto cuentan con documentación decir hasta qué punto es seguro llamar a métodos

simultáneamente desde varios subprocesos. Usted necesita tener en cuenta, sin embargo, que

muchas de las clases de especifican que sus métodos estáticos son thread‐safe, y sus métodos de

instancia no son. Para llamar a los métodos de instancia debe puede utilizar su propio bloqueo para

asegurarse de sólo un hilo a la vez está llamando, o en muchos casos la clase proporciona un

método "Sincronizada" que va a crear un contenedor de sincronización alrededor de una instancia

de objeto. Necesitará conocer algunos de los parámetros de rendimiento de su implementación de hilos.

¿Cuál es el coste de crear un hilo? ¿Cuál es el costo de mantener un subproceso bloqueado en

existencia? ¿Cuál es el costo de un cambio de contexto? ¿Cuál es el costo de un "cerradura"

declaración cuando el objeto es no bloqueado? Sabiendo esto, usted será capaz de decidir en qué

medida es factible o útil para añadir subprocesos adicionales a su programa. Introducción a la

programación con C# hilos . 31

Page 34: concurrente.pdf

6.6. posibles problemas con la adición de hilos de rosca

Necesitas un poco de cuidado en la adición de hilos de rosca del ejercicio, o usted encontrará que su

programa se ejecuta más despacio en lugar de más rápido. Si tienes significativamente más subprocesos listos para ejecutarse que hay procesadores,

generalmente encontrará que el rendimiento de su programa se degrada. Esto es en parte porque la

mayoría programadores del hilo de rosca son muy lentos en tomar decisiones re‐scheduling

general. Si hay un procesador inactivo esperando su hilo, el programador puede probablemente

llegarlo bastante rápido. Pero si el hilo tiene que ser puesto en una cola y después cambió a un

procesador en lugar de algún otro hilo, será más caro. Un segundo efecto es que si tienes un

montón de threads ejecutándose son más propensos al conflicto sobre las cerraduras o los recursos

gestionados por sus variables de condición. Sobre todo, cuando añades hilos para mejorar la estructura del programa (por ejemplo manejar

dispositivos lentos o rápidamente, o para las invocaciones de RPC en respuesta a eventos de la

interfaz de usuario) no encontrará este problema; Pero cuando añades hilos para propósitos de

rendimiento (por ejemplo, realizar varias acciones en paralelo, o aplazar la obra o utilizando multi‐

processors), usted necesitará preocuparse si usted sobrecarga el sistema. Pero quiero destacar que esta advertencia se aplica sólo a las roscas que están dispuestas a

correr. El gasto de tener hilos bloqueados esperando en un objeto es generalmente menos

significativo, siendo sólo la memoria utilizada para las estructuras de datos del planificador y la

pila del subproceso. Well‐written multi‐threaded aplicaciones suelen tienen un gran número de

subprocesos bloqueados (50 no es infrecuente). En la mayoría de los sistemas, las instalaciones de creación y terminación de hilo no son

baratas. La implementación de hilos se encargará probablemente para guardar en caché unos

cadáveres de hilo terminada, para que usted no paga por creación de pila en cada encrucijada, pero

sin embargo creando un nuevo hilo probablemente tendrán un costo total de dos o tres decisiones

re‐scheduling. Así que usted no debería horquilla demasiado pequeño un cómputo en un

subproceso independiente. Una medida útil de una implementación de hilos en un multi‐processor

es el cómputo más pequeño por lo que es rentable a un hilo de la bifurcación. A pesar de estas precauciones, tenga en cuenta que mi experiencia ha sido que los

programadores son más propensos a errar creando demasiados pocos hilos como creando

demasiados.

7. USO DE INTERRUPCIÓN: DESVIAR EL FLUJO DE CONTROL

El propósito del método "Interrupción" de un hilo es decir el hilo que debe abandonar lo que está

haciendo y que control de volver a una abstracción de nivel superior, probablemente el único que

hizo la llamada de "Interrumpir". Por ejemplo, en un multi‐processor podría ser útil para varios

competidores algoritmos para resolver el mismo problema de la bifurcación, y cuando termina el

primero de ellos anula los demás. O usted puede embarcarse en un cómputo largo (por ejemplo,

una consulta a un servidor de base de datos remota), pero anularlo si el usuario hace clic en un

Cancelar botón. O tal vez quieras limpiar un objeto que utiliza algunos hilos demonio internamente. Por ejemplo, podríamos añadir un "Disponer"método"PipelinedRasterizer"para terminar sus dos

hilos cuando hayamos terminado de usar el"PipelinedRasterizer"32 . Introducción a la programación con

C# hilos

Page 35: concurrente.pdf

* El recolector de basura podría notar que si la única referencia a un objeto es de hilos que no son

accesibles desde el exterior y que están bloqueados en una espera con ningún tiempo de espera, entonces el

objeto y los hilos pueden desecharse. Lamentablemente, recolectores de basura reales no inteligentes. objeto . Observe que si no hacemos esto el "PipelinedRasterizer" objeto no se nunca recogerán

la basura, porque se hace referencia a sus propias roscas daimonion.*

clase PipelinedRasterizer: IDisposable {

public void Dispose() { cerradura(este) { si (t1! = null) t1.Interrupt(); si(t2! = null) t2.Interrupt(); T1 = null; T2 = null; } }

vacío Rasterizer() { pruebe { mientras (true) { char c = (char) (rasterizeQ.Dequeue()); / /

convertir caracteres a un mapa de bits... Mapa de bits, b = f.Render(c); paintQ.Enqueue(b); {}} catch (ThreadInterruptedException) {}}

vacío Painter() { pruebe { mientras (true) {Bitmap b = (Bitmap)(paintQ.Dequeue()); / / pintura de

mapa de bits en el dispositivo de gráficos... d.PaintBitmap(b);}} atrapar(ThreadInterruptedException) { } }

… } clase PipelineRasterizer

Hay veces cuando quiera interrumpir un subproceso que se está realizando un cálculo largo pero

no llamadas de "Espera". La documentación de C# es un poco vaga acerca de cómo hacer esto, pero

seguramente puede conseguir este efecto si el cómputo largo ocasionalmente llama

"Thread.Sleep(0)". Diseños anteriores tales como Java y Modula incluyen mecanismos diseñados

específicamente para permitir que un subproceso sondear a Una introducción a la programación con C#

hilos . 33

Page 36: concurrente.pdf

ver si hay una interrupción pendiente (por ejemplo, si una llamada de "espera" lanzaría el

"interrumpido" excepción). Modula también permite dos tipos de espera: se y non‐alertable. Esto permitió un espacio de su

programa a escribirse sin preocupación por la posibilidad de una excepción repentina que surjan.

En C# todas llamadas de "Monitor.Wait" son interrumpible (como son las llamadas correspondientes

en Java), y para ser correcta debe tampoco arreglan eso todas llamadas de "espera" están preparados

para el "interrumpido" ser excepción, o usted debe verificar que no se llamará al método de

interrupción en las roscas que están realizando los espera. Esto no debería ser demasiado de una

imposición en su programa, puesto que ya necesitas restaurar invariantes monitor antes de llamar

"espera". Sin embargo tienes que tener cuidado que si se produce la excepción "Interrumpido" luego

libere recursos retenidos en la pila de Marcos ser desenrollado, presumiblemente por escrito

correspondiente "finalmente" declaraciones. El problema con hilo de interrupciones es que son, por su propia naturaleza, intrusivo.

Usándolos tenderá a hacer su programa que menos bien estructurado. Un flujo de straightforward‐

looking de control en un subproceso de repente puede ser desviado a causa de una acción iniciada

por otro subproceso. Este es otro ejemplo de una instalación que hace más difícil verificar la

exactitud de un pedazo de programa por la inspección de local. A menos que las alertas se utilizan

con mucha moderación, harán que tu programa ilegible, insostenible y quizás incorrecto. Te

recomiendo utilizar pocas interrupciones y que el "interrumpir" método debería llamarse solamente

de la abstracción donde se creó el hilo. Por ejemplo, un paquete no debe interrumpir hilo de un

oyente que le pasa a estar ejecutando dentro del paquete. Este Convenio le permite ver una

interrupción como una indicación de que el hilo debe terminar completamente, pero limpio. a menudo hay mejores alternativas al uso de interrupciones. Si sabes qué objeto está esperando

un hilo, más simplemente puede prod estableciendo una bandera booleana y llamando

"Monitor.Pulse". Un paquete podría proporcionar puntos de entrada adicional cuyo propósito es

prod un subproceso bloqueado dentro del paquete en una espera de largo plazo. Por ejemplo, en

lugar de implementar "PipelinedRasterizer.Dispose" con el "interrumpir" mecanismo nos podríamos

haber añadido un "disponer" método para el "cola" de la clase y que. Las interrupciones son más útiles cuando no sabes exactamente lo que está pasando. Por

ejemplo, el subproceso de destino podría ser bloqueado en cualquiera de varios paquetes, o dentro

de un solo paquete podría estar esperando en cualquiera de los varios objetos. En estos casos una

interrupción es sin duda la mejor solución. Aun cuando existen otras alternativas disponibles, sería

mejor utilizar interrupciones sólo porque son un solo esquema unificado para provocar la

terminación de subprocesos. No hay que confundir "Interrumpir" con el mecanismo absolutamente distinto llamado

"Abortar", que describiré más adelante. Sólo "interrumpir" permite interrumpir el hilo en un lugar

well‐defined, y es la única manera que el hilo tendrá alguna esperanza de la restauración de los

invariantes en sus variables compartidas.

8. OTRAS TÉCNICAS

La mayoría de los paradigmas de programación para el uso de hilos de rosca es muy simple. Varios

de ellos he descrito anteriormente; usted descubrirá muchos otros como usted gana 34 . Introducción

a la programación con C# hilos

Page 37: concurrente.pdf

experiencia. Algunas de las técnicas útiles son mucho menos obvias. Esta sección describe algunos

de estos menos obvios.

8.1.-llamadas

La mayor parte del tiempo la mayoría de los programadores construyen sus programas usando

capas de abstracciones. Abstracciones de nivel superiores llaman sólo baja los y abstracciones en el

mismo nivel no llamarnos. Todas las acciones se inician en el nivel superior. Esta metodología lleva bastante bien a un mundo con concurrencia. Usted puede arreglar que

cada subproceso honrará los límites de la abstracción. Hilos daimonion permanente dentro de una

abstracción inician llamadas a niveles inferiores, pero no a niveles más altos. Las capas de

abstracción tiene el beneficio añadido que forma un orden parcial, y este orden es suficiente para

evitar los interbloqueos cuando bloquee objetos, sin ningún cuidado adicional del programador. Este top‐down puramente capas no es satisfactorio cuando las acciones que afectan las

abstracciones high‐level pueden iniciarse en una capa baja en su sistema. Un ejemplo frecuente de

esto es en el lado receptor de comunicaciones de la red. Otros ejemplos son introducidos por el

usuario, y cambios de estado espontáneo en los dispositivos periféricos. Considere el ejemplo de una trata de paquete de comunicaciones de paquetes entrantes de una

red. Aquí hay típicamente tres o más capas de envío (correspondientes a las capas de enlace, red y

transporte de datos de terminología OSI). Si se intenta mantener una top‐down llamada jerarquía,

usted encontrará que usted incurrir en un cambio de contexto en cada una de estas capas. El hilo

que desea recibir información de su conexión de capa de transporte no puede ser el hilo que envía

un paquete entrante de Ethernet, ya que el paquete Ethernet podría pertenecer a una conexión

diferente, o un protocolo diferente (por ejemplo, UDP en lugar de TCP) o una en familia conjunto

protocolo diferente (por ejemplo, DECnet en lugar de IP). Los implementadores muchos han

tratado de mantener esta estratificación para la recepción de paquetes, y el efecto ha sido

uniformemente mala actuación — dominado por el costo de cambios de contexto. La técnica alternativa es conocida como "up‐calls" [6]. En esta metodología, mantener una piscina

de hilos dispuestos a recibir paquetes de capa (por ejemplo, Ethernet) de enlace de datos entrantes.

El hilo receptor envía el tipo de protocolo de Ethernet y llama a la capa de red (por ejemplo, DECnet

o IP), donde despacha otra vez y llama a la capa de transporte (por ejemplo, TCP), donde hay un

mensaje final a la conexión apropiada. En algunos sistemas, este paradigma de up‐call se extiende

en la aplicación. La atracción es de alto rendimiento: hay no hay cambios de contexto innecesarios. Usted paga para esta actuación. Como de costumbre, la tarea del programador se ha hecho más

complicada. En parte esto es porque cada capa tiene ahora una interfaz up‐call, así como la interfaz

tradicional down‐call. Pero también el problema de sincronización se ha vuelto más delicado. En un

sistema puramente top‐down está bien sostener la cerradura de una capa mientras que llamando

una capa más baja (a menos que la capa más baja podría bloquear un objeto esperando alguna

condición para convertirse en verdadero y causar así el tipo de bloqueo de monitor anidados que

discutimos anteriormente). Pero cuando usted hace una up‐call fácilmente puede provocar un

estancamiento que involucra sólo el Introducción a la programación con C# hilos . 35

Page 38: concurrente.pdf

cerraduras — si un hilo de up‐calling mantiene un bloqueo de nivel inferior necesita adquirir

un bloqueo en una abstracción de nivel superior (desde el bloqueo podría ser celebrado por algún

otro hilo que está haciendo un down‐call). En otras palabras, la presencia de up‐calls hace más

probable que usted viola la regla de orden parcial para el bloqueo de objetos. Para evitar esto, debe

evitar generalmente mantienen un bloqueo mientras haciendo un up‐call (pero esto es más fácil

decirlo que hacerlo).

8.2. versión sellos y almacenamiento en caché

A veces simultaneidad puede hacerlo más difícil utilizar la información almacenada en caché. Esto

puede ocurrir cuando un subproceso independiente ejecutando en un nivel inferior en su sistema

invalida alguna información a un hilo que se está ejecutando actualmente en un nivel superior. Por

ejemplo, puede cambiar la información sobre un volumen de disco — ya sea por problemas de

hardware o porque el volumen ha sido eliminado y reemplazado. Puede utilizar up‐calls para

invalidar las estructuras de la memoria caché en el nivel superior, pero esto no invalida estado

localmente por un hilo. En el ejemplo más extremo, un subproceso puede obtener información de

un caché y estar a punto de llamar a una operación en el nivel inferior. Entre el momento en que la

información proviene de la caché y el momento en que la llamada se produce en realidad, la

información podría haberse convertido en no válida. Una técnica conocida como "sellos versión" puede ser útil aquí. En la abstracción de bajo nivel

que mantener un contador asociado con los datos verdaderos. Cuando cambian los datos, se

incrementa el contador. (Suponga que el contador es tan grande que nunca desbordamiento).

Cuando se expida una copia de algunos de los datos a un nivel superior, es acompañado por el

valor actual del contador. Si el código de nivel superior es almacenar en caché los datos, almacena

en caché el valor del contador asociado demasiado. Cuando haga una llamada a nivel inferior, y la

llamada o sus parámetros dependen de datos previamente obtenidos, se incluye el valor asociado

del contador. Cuando el nivel bajo recibe dicha llamada, compara el valor entrante del contador con

el valor actual de la verdad. Si son diferentes devuelve una excepción para el nivel superior, que

entonces sabe que re‐consider su llamada. (A veces, puede proporcionar los datos nuevos con la

excepción). Por cierto, esta técnica también es útil cuando se mantienen en la memoria caché de

datos a través de un sistema distribuido.

8.3. trabajo equipos (subprocesos)

Hay situaciones que son mejor descritas como "una vergüenza de paralelismo", cuando usted

puede estructurar su programa a tener muchísimo más concurrencia que pueden ser eficientemente

acomodados en su máquina. Por ejemplo, un compilador implementado usando concurrencia

estarían dispuesto a utilizar un subproceso independiente para compilar cada método, o incluso

cada declaración. En tales situaciones, si se crea un subproceso para cada acción terminará con

tantos hilos que el programador se convierte absolutamente ineficaz, o tanta que tiene numerosos

conflictos de bloqueo, o tantos que ejecute fuera de memoria para las pilas. Tu elección aquí es más restringida en la bifurcación, o usar una abstracción que controlará su

bifurcación para ti. Tan una abstracción primero fue descrita en papel Vandevoorde y Roberts [20]

y está disponible para programadores de C# a través de los métodos de la "ThreadPool" clase: 36 . Introducción a la programación con C# hilos

Page 39: concurrente.pdf

público clase sellada ThreadPool {...}

La idea básica es enqueue solicitudes para actividad asincrónica y tener un grupo fijo de hilos que

realizan las peticiones. La complejidad viene en gestionar las peticiones, sincronización entre ellos y

la coordinación de los resultados. Cuidado, sin embargo, que la C# "ThreadPool"clase utiliza métodos completamente estáticos –

hay una única agrupación de hilos para toda la aplicación. Esto está bien si las tareas que le das al

grupo de subprocesos son puramente computacionales; Pero si las tareas pueden incurrir en

retrasos (por ejemplo, haciendo una llamada RPC de red) bien podría encontrar la abstracción

Shagun inadecuada. Es una propuesta alternativa, que no he visto aún en la práctica, para implementar

"Thread.Create"y"Thread.Start"de tal manera que ellos diferir en realidad creando el nuevo

subproceso hasta que haya un procesador disponible para ejecutarlo. Esta propuesta se ha llamado

"vagos que se bifurcan".

8.4. superposición de cerraduras

Los siguientes son momentos cuando puede usar más de una cerradura para algunos datos.

A veces cuando es importante permitir concurrente acceso de lectura a algunos datos, mientras

que sigue usando la exclusión mutua para acceso de escritura, bastará con una técnica muy sencilla.

Usar bloqueos dos (o más), con la regla de que ningún hilo sosteniendo una cerradura puede leer

los datos, pero si un subproceso quiere modificar los datos debe adquirir las cerraduras ambos (o

todos). Otra técnica de bloqueo superpuestas se utiliza a menudo para recorrer una lista enlazada. Una

cerradura para cada elemento y adquirir la cerradura para el elemento siguiente antes de soltar el

uno para el elemento actual. Esto requiere el uso explícito de la "Enter"y"salida" métodos, pero

puede producir mejoras en el rendimiento espectacular reduciendo conflictos bloqueo.

9. AVANZADO C# CARACTERÍSTICAS

a lo largo de este trabajo he restringido la discusión a un pequeño subconjunto de los

"System.Threading"espacio de nombres. Recomiendo encarecidamente que restringe su

programación a este subconjunto (además de la "System.Threading.ReaderWriterLock" clase) tanto

como puedas. Sin embargo, el resto del espacio de nombres fue definido para un propósito, y hay

veces cuando usted necesitará utilizar partes de él. Esta sección describe las otras características. Hay variantes del "Monitor.Wait"toma un argumento adicional. Este argumento especifica un

intervalo de tiempo de espera: Si el hilo no es despertado por "pulso","PulseAll"o"interrumpir"dentro

de ese intervalo, entonces la llamada"espera" retorna de todos modos. En tal situación la llamada

devuelve "falso". Hay una manera alternativa para notificar a un subproceso que debe desistir: llamas de la rosca

"Abortar"método de. Esto es mucho más drástico y disruptiva que "interrumpir", porque se produce

una excepción en un punto arbitrario y ill‐defined (en vez de sólo en llamadas de

"esperar","dormir"o"Únete"). Esto significa que en general será imposible para el hilo restaurar

invariantes. Dejará su compartida Introducción a la programación con C# hilos . 37

Page 40: concurrente.pdf

datos de en Estados ill‐defined. El uso razonable solamente de "abortar" es terminar un

cómputo ilimitado o un non‐interruptible espera. Si usted tiene que recurrir a "abortar" tendrá que

tomar medidas para re‐initialize o descarte afectado compartida variables. Varias clases en "System.Threading"corresponden a objetos implementados por el núcleo. Estos

incluyen "AutoResetEvent","ManualResetEvent","Mutex", y "WaitHandle". El beneficio real sólo que

obtendrá del uso de estos es que pueden utilizarse para sincronizar entre subprocesos en múltiples

espacios de direcciones. También habrá momentos cuando necesitas sincronizar con código

heredado. El "Enclavijado"clase puede ser útil para operaciones simples de incremento, decremento o

intercambio atómicas. Recuerda que solo puedes hacerlo si su invariante implica sólo una sola

variable. "Interlocked" no te ayudará cuando participa más de una variable.

10. EL PROGRAMA DE CONSTRUCCIÓN

Un programa exitoso debe ser útil, correcta, vivo (como se define a continuación) y eficiente. El uso

de simultaneidad puede afectar a cada uno de ellos. He discutido muchas técnicas en las secciones

anteriores que te ayudará. Pero, ¿cómo sabrá si han tenido éxito? La respuesta no es clara, pero en

esta sección puede ayudarle a descubrirlo. El lugar donde simultaneidad puede afectar la utilidad está en el diseño de las interfaces para

paquetes de bibliotecas. Se deben diseñar sus clases con la suposición de que las llamadas utilizarán

varios subprocesos. Esto significa que debe asegurarse de que todos los métodos son re‐entrant del

hilo de rosca (es decir, pueden ser llamados por varios subprocesos simultáneamente), incluso si

esto significa que cada método inmediatamente adquiere un bloqueo compartido solo. Usted no

debe devolver resultados en variables estáticas compartidas, ni en almacenamiento compartido

asignado. Los métodos deben ser sincrónicos, no regresar hasta que los resultados estén disponibles

— si tu llamador quiere hacer otros trabajos mientras tanto, puede hacerlo en otros subprocesos.

Incluso si usted no tiene actualmente ningún cliente de multi‐threaded para una clase, le

recomiendo que siga estas pautas para que evitará problemas en el futuro. No todos están de acuerdo con el párrafo anterior. En particular, la mayoría de los métodos de

instancia en las bibliotecas con C# (aquellos en el CLR y la plataforma .net SDK) no es thread‐safe.

Asumen que una instancia del objeto se llama desde sólo un hilo a la vez. Algunas de las clases

proporcionan un método que devolverá una instancia de objeto correctamente sincronizada, pero

muchos no lo hacen. El motivo de esta decisión de diseño que es el costo de la "cerradura"

declaración se creía que era demasiado alto como para usarlo donde sea necesario. Personalmente,

no estoy de acuerdo con esto: los gastos de envío un programa incorrectamente sincronizado

pueden ser mucho mayor. En mi opinión, es la solución correcta implementar la "cerradura"

declaración de una manera que es suficientemente barata. Se conocen las técnicas para hacer este

[5]. Por "correcto" es decir que si su programa eventualmente produce una respuesta, será el

definido por la especificación. Su entorno de programación es poco probable que proporcionan

mucha ayuda aquí más allá de lo que ya prevé programas secuenciales. Sobre todo, debe ser

fastidioso de asociar cada dato con la cerradura de uno (y único). Si no prestas atención constante, 38 . Introducción a la programación con C# hilos

Page 41: concurrente.pdf

tu tarea será inútil. Si utiliza correctamente las cerraduras, y utilice siempre espera para objetos

en el estilo recomendado (re‐testing el valor booleano expresión después de volver del "espera"),

entonces es poco probable equivocarse. Por "vivir", es decir que su programa eventualmente producirá una respuesta. Las alternativas

son ciclos infinitos o estancamiento. No puedo con ciclos infinitos. Creo que las notas de las

secciones anteriores le ayudará a evitar interbloqueos. Pero si fracasan y producir un interbloqueo,

debería ser bastante fácil de detectar. Por "eficiente", es decir que el programa hará buen uso de los recursos informáticos disponibles

y por lo tanto producirá su respuesta rápidamente. Otra vez, las sugerencias en las secciones

anteriores deberían ayudarte a evitar el problema de concurrencia afectando negativamente su

rendimiento. Y otra vez, su entorno de programación para darle un poco de ayuda. Fallos de

funcionamiento son el más insidioso de los problemas, ya que usted podría no incluso notar que

tienes. El tipo de información que necesita obtener incluye estadísticas sobre conflictos de bloqueo

(por ejemplo, con qué frecuencia subprocesos han tenido que bloquear con el fin de adquirir esta

cerradura, y cuánto tiempo después tuvieron que esperar para un objeto) y de los niveles de

concurrencia (por ejemplo, ¿cuál fue el número promedio de subprocesos listos para ejecutar en el

programa, o qué porcentaje del tiempo estaban listos "n" hilos). En un mundo ideal, su entorno de programación proporcionaría un potente conjunto de

herramientas para ayudarle a lograr la corrección, liveness y eficiencia en el uso de simultaneidad.

Desafortunadamente en realidad lo más que puedas encontrar hoy en día es las características

habituales de un depurador simbólico. Es posible construir herramientas mucho más potentes,

como especificación de idiomas con comprobadores de modelo para verificar lo que el programa

hace [11], o herramientas que detectan acceder a variables sin las cerraduras apropiadas [19]. Hasta

ahora, estas herramientas no son ampliamente disponibles, aunque eso es algo que espero que sea

capaces de solucionar. Una última advertencia: no enfatizar la eficiencia a expensas de corrección. Es mucho más fácil

comenzar con un programa correcto y trabajar por lo que es eficiente, que comenzar con un

programa eficiente y el trabajo de hacer lo correcto.

11. OBSERVACIONES

Escribir programas concurrentes tiene una reputación de ser exóticos y difíciles. Creo que no es ni.

Se necesita un sistema que le proporciona buenas primitivos y bibliotecas adecuadas, usted necesita

un cuidado básico y esmero, necesitas un arsenal de técnicas útiles y tienes que saber de los errores

comunes. Espero que este artículo te ha ayudado a compartir mi creencia. Butler Lampson, Mike Schroeder, Bob Stewart y Bob Taylor me llevó a escribir este artículo (en

1988), y Chuck Thackerme convenció a revisarlo para C# (en 2003).Si encontraron útil, darles las

gracias.