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
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.*
* 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
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¿ "?
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
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
*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
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
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
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
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é
subproceso ha bloqueado el objeto y con qué frecuencia. El objeto permanece bloqueado Introducción a la programación con C# hilos . 11
(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
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
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
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
* 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; }
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
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,
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
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).*
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
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
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
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
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
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
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
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
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
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
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
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
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
* 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
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
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
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
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
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
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.