|
|||
require 'net/http'
pages = %w( www.rubycentral.com
www.awl.com
www.pragmaticprogrammer.com
)
threads = []
for page in pages
threads << Thread.new(page) { |myPage|
h = Net::HTTP.new(myPage, 80)
puts "Fetching: #{myPage}"
resp, data = h.get('/', nil )
puts "Got #{myPage}: #{resp.message}"
}
end
threads.each { |aThread| aThread.join }
|
|
|
Fetching: www.rubycentral.com Fetching: www.awl.com Fetching: www.pragmaticprogrammer.com Got www.rubycentral.com: OK Got www.pragmaticprogrammer.com: OK Got www.awl.com: OK |
Thread.new
.
Recibe como parámetro un código que contiene lo que se ejecutará en cada hilo.
En nuetro caso, el bloque usa la librería net/http para coger la página principal de
cada uno de los sitios indicados. La traza muestra claramente cómo las solicitudes
se ejecutan en paralelo.
Cuando creamos el hilo, le pasamos la página solicitada como parámetro.
Este parámetro es pasado al bloque como myPage.
¿Por qué hacemos ésto, y no cogemos directamente el valor de la variable
page desde el interior del bloque?
Cada hilo comparte todas las variables locales, globales y de instancia
que existen en el momento en que se inicia.
Alguien con un hermano pequeño
te podría decir que compartir no es algo bueno. En este caso, los tres
hilos compartirían la variable page. El primer hilo empieza,
y page se establece a http://www.rubycentral.com. Mientras
tanto, el bucle que genera los hilos se sigue ejecutando. En la segunda
iteración, page se establece a http://www.awl.com. Si el primer
hilo no había terminado de usar la variable page, leerá
de repente su nuevo valor. Éstos bugs son difíciles de controlar.
Sin embargo, las variables locales creadas dentro del bloque del hilo son realmente
locales a ese hilo---cada uno de los hilos tendrá su propia copia de las
variables. En nuestro caso, la variable myPage se inicializará
cada vez que un hilo se cree, y cada hilo tendrá su propia copia de las
direcciones de las páginas.
join en cada hilo que creamos?
Cuando un programa Ruby termina, todos los hilos en ejecución se matan,
independientemente de su estado. Sin embargo, puedes esperar a que termine un
hilo en particular llamándo al método del hilo
Thread#join
.
El hilo que llama al método quedará bloqueado hasta que el hilo llamado termina. Invocando
a join en cada uno de los hilos que solicitan la página, te aseguras de que
las tres peticiones se completan antes de que finalice el programa principal.
Además de join, existen unas cuantas sentencias más que se utilizan
para controlar los hilos. En primer lugar, el hilo actual es siempre
accesible mediante
Thread.current
. Puedes obtener
una lista de todos los hilos con
Thread.list
, que devuelve todos
los objetos Thread que están en ejecución o parados. Para conocer el
estado de un hilo en particular, puedes usar
Thread#status
y
Thread#alive?
.
También puedes ajustar la prioridad de un hilo mediante
Thread#priority=
. Los hilos de mayor prioridad
se ejecutarán antes que los de menor prioridad. Hablaremos más tarde sobre la planificación de hilos, y sobre
comenzarlos y pararlos.
Thread proporciona una
funcionalidad especial que permite crear y acceder a variables locales a un hilo
por el nombre. Simplemente tienes que tratar el objeto del hilo como si fuera
una Hash, guardando elementos mediante []= y recuperándolos
con []. En este ejemplo, cada hilo guarda el valor actual de
la variable count en una variable local con la clave
mycount. (Existe una situación incontrolada en este código, pero no hemos
hablado de sincronización todavía, por lo que de momento lo ignoraemos.)
count = 0
arr = []
10.times do |i|
arr[i] = Thread.new {
sleep(rand(0)/10.0)
Thread.current["mycount"] = count
count += 1
}
end
arr.each {|t| t.join; print t["mycount"], ", " }
puts "count = #{count}"
|
8, 0, 3, 7, 2, 1, 6, 5, 4, 9, count = 10 |
count guardado por cada uno. Para hacerlo
aún más interesante, hemos hecho que cada hilo espere un tiempo
aleatorio antes de guardar el valor.
abort_on_exception está a false, el valor
por defecto, una excepción sin capturar simplemente mata el hilo
actual---los restantes siguen ejecutándose. En el ejemplo siguiente,
el hilo número 3 aborta y no produce ninguna salida. Sin embargo,
todavía puedes ver la traza del resto de los hilos.
threads = []
6.times { |i|
threads << Thread.new(i) {
raise "Boom!" if i == 3
puts i
}
}
threads.each {|t| t.join }
|
01 2 45prog.rb:4: Boom! (RuntimeError) from prog.rb:8:in `join' from prog.rb:8 from prog.rb:8:in `each' from prog.rb:8 |
abort_on_exception a true, una excepción
no capturada finalizará todos los hilos. Una vez que el hilo 3 muere,
no se produce más salida.
Thread.abort_on_exception = true
threads = []
6.times { |i|
threads << Thread.new(i) {
raise "Boom!" if i == 3
puts i
}
}
threads.each {|t| t.join }
|
01 2 prog.rb:5: Boom! (RuntimeError) from prog.rb:7:in `initialize' from prog.rb:7:in `new' from prog.rb:7 from prog.rb:3:in `times' from prog.rb:3 |
Thread proporciona una serie de métodos para
controlar el planificador de hilos. Invocando
Thread.stop
se detiene el hilo actual, mientras
Thread#run
se preocupa de que un hilo particular se
ejecute.
Thread.pass
desplanifica el hilo actual, permitiendo
a otros ejecutarse, y
Thread#join
y
Thread#value
detienen el
hilo invocador hasta que el hilo llamado finalice.
Podemos mostrar estas características en el siguiente código, que no
hace nada especialmente interesante.
t = Thread.new { sleep .1; Thread.pass; Thread.stop; }
|
||
t.status
|
» |
"sleep"
|
t.run
|
||
t.status
|
» |
"run"
|
t.run
|
||
t.status
|
» |
false
|
true (usando el método
Thread.critical=
),
el planificador no dejará que ningún otro
hilo existente se ejecute.
Sin embargo, ésto no impide que nuevos hilos se creen y ejecuten.
Ciertas operaciones sobre hilos (como parar o matar
un hilo, dormirlo, o lanzar una excepción) pueden provocar que un
hilo se ejecute aún estando en una sección crítica.
Usar
Thread.critical=
directamente es en cierto modo factible,
pero es terriblemente desaconsejado. Afortunadamente, Ruby dispone de varias
alternativas. De todas ellas, las dos mejores son la clase Mutex y la
clase ConditionVariable, disponibles en el módulo de la librería thread; consulta
la documentáción que comienza en la página 457.
Mutex es una clase que implementa un sencillo
semáforo para programar acceso mutuamente exclusivo a recursos compartidos.
Ésto es, sólamente un hilo puede mantener el bloqueo en un momento dado. Los demás
hilos pueden escoger entre esperar a que se quite el bloqueo,
o simplemente dar un mensaje de error indicando que existe un bloqueo.
Los mutex se usan normalmente cuando las operaciones sobre los datos tienen que ser atómicas.
Imagínate que necesitamos actualizar dos variables durante una transacción. Podemos
simular ésto en un sencillo programa incrementando unos cuantos contadores. Se supone que
las actualizaciones deberían ser atómicas---el resto del mundo no debería ver nunca valores diferentes
en los contadores. Sin ningún tipo de control con semáforos,
ésto simplemente no sucede.
count1 = count2 = 0
|
||
difference = 0
|
||
counter = Thread.new do
|
||
loop do
|
||
count1 += 1
|
||
count2 += 1
|
||
end
|
||
end
|
||
spy = Thread.new do
|
||
loop do
|
||
difference += (count1 - count2).abs
|
||
end
|
||
end
|
||
sleep 1
|
||
Thread.critical = 1
|
||
count1
|
» |
184846
|
count2
|
» |
184846
|
difference
|
» |
58126
|
count1 y count2 inconsistentes.
Afortunadamente podemos arreglar ésto usando un mutex.
require 'thread' mutex = Mutex.new count1 = count2 = 0 difference = 0 counter = Thread.new do loop do mutex.synchronize do count1 += 1 count2 += 1 end end end spy = Thread.new do loop do mutex.synchronize do difference += (count1 - count2).abs end end end |
sleep 1
|
||
mutex.lock
|
||
count1
|
» |
21192
|
count2
|
» |
21192
|
difference
|
» |
0
|
require 'thread'
mutex = Mutex.new
cv = ConditionVariable.new
a = Thread.new {
mutex.synchronize {
puts "A: I have critical section, but will wait for cv"
cv.wait(mutex)
puts "A: I have critical section again! I rule!"
}
}
puts "(Later, back at the ranch...)"
b = Thread.new {
mutex.synchronize {
puts "B: Now I am critical, but am done with cv"
cv.signal
puts "B: I am still critical, finishing up"
}
}
a.join
b.join
|
A: I have critical section, but will wait for cv(Later, back at the ranch...) B: Now I am critical, but am done with cv B: I am still critical, finishing up A: I have critical section again! I rule! |
monitor.rb y sync.rb en el subdirectorio lib de la
distribución.
system y los acentos graves.
system("tar xzf test.tgz")
|
» |
tar: test.tgz: Cannot open: No such file or directory\ntar: Error is not recoverable: exiting now\ntar: Child returned status 2\ntar: Error exit delayed from previous errors\nfalse
|
result = `date`
|
||
result
|
» |
"Sun Jun 9 00:08:50 CDT 2002\n"
|
Kernel::system
ejecuta el comando
recibido en un subproceso; devuelve true si el comando se
encontró y ejecutó correctamente, y false en caso contrario. Si ocurre algún
fallo, encontrarás el código de salida del subproceso en la variable global
$?.
Un problema con system es que la salida del comando
va al mismo lugar que la salida de tu programa, que puede
ser algo que no desees. Para capturar la salida estándar de un
subproceso, puedes usar los acentos graves, como en `date` en
el ejemplo anterior. Recuerda que quizá podrías necesitar usar
String#chomp
en result para eliminar los caracteres
especiales de la línea de edición.
Bien, ésto está bien para casos sencillos---podemos ejecutar otros
procesos y obtener su código de salida. Pero muchas veces necesitamos
un poco más de control sobre ésto. Podriamos querer entablar una conversación
con el subproceso enviándole o recogiendo datos de él.
El método
IO.popen
hace precisamente ésto. El método
popen ejecuta un comando como subproceso y conecta las salida y entrada estándar
al objeto Ruby IO. Se puede escribir en el objeto
IO, y el subproceso leerá de él en su entrada
estándar. Y cualquier cosa que él proceso escriba estará disponible en el programa
mediante la lectura en el objeto IO.
Por ejemplo, una de las aplicaciones más útiles en nuestros sistemas es
pig, un programa que lee palabras desde la entrada estándar
y las imprime en Latín pig (o atinlay igpay). Podríamos usar este programa
cuando queramos que nuestros programas Ruby den su salida de modo que un niño
de 5 años no sea capaz de entenderla.
pig = IO.popen("pig", "w+")
pig.puts "ice cream after they go to bed"
pig.close_write
puts pig.gets
|
iceway eamcray afterway eythay ogay otay edbay |
pig no finaliza
la salida que escribe. Nuestro intento inicial en este ejemplo,
fue hacer pig.puts seguido pig.gets, provocando un bloqueo indefinido.
El programa pig procesaba nuestra entrada, pero la respuesta nunca
aparecía escrita en la tubería. Tuvimos que insertar la línea pig.close_write.
Ésto manda un carácter de fin de fichero a la entrada estándar de pig, y la salida
que estamos esperando se actualiza una vez que pig termina.
Existe un truco más con popen. Si el comando que le pasas
es un signo menos (``--''), popen creará un nuevo
intérprete Ruby. Tanto éste como el intérprete original continuarán
justo después del valor devuelto por popen. El proceso orignal
recibirá un objeto IO, mientras que el hijo recibirá nil.
pipe = IO.popen("-","w+")
if pipe
pipe.puts "Get a job!"
$stderr.puts "Child says '#{pipe.gets.chomp}'"
else
$stderr.puts "Dad says '#{gets.chomp}'"
puts "OK"
end
|
Dad says 'Get a job!' Child says 'OK' |
popen, las llamadas tradicionales de Unix
Kernel::fork
,
Kernel::exec
, y
IO.pipe
estarán
disponibles en las plataformas que las soporten. Siguiendo el convenio en los nombres de archivos
de muchos métodos de IO y en
Kernel::open
,
también se crearán subprocesos si añades un ``|''
como primer carácter del nombre
del archivo (mira la introducción de la clase IO en la página 325 para
más detalles). Cabe destacar que no puedes crear tuberías usando
File.new
; ésto es solo para ficheros.
exec("sort testfile > output.txt") if fork == nil
# The sort is now running in a child process
# carry on processing in the main program
# then wait for the sort to finish
Process.wait
|
Kernel::fork
devuelve el id del nuevo proceso al padre, y
nil al hijo, por lo que en nuestro ejemplo, el hijo realiza la llamada a
Kernel::exec
y ejecuta sort. Un tiempo más tarde, hacemos una llamada a
Process::wait
, que espera a que sort termine (y
devuelve su identificador de proceso).
Si prefieres que se te notifique cuando el hijo termina (en lugar
de esperar activamente por él), puedes instalar un manejador de señales usando
Kernel::trap
(descrito en la página 427). A continuación ponemos
un manejador para la señal SIGCLD, que es la señal enviada cuando sucede ``la muerte de un proceso
hijo.''
trap("CLD") {
pid = Process.wait
puts "Child pid #{pid}: terminated"
exit
}
exec("sort testfile > output.txt") if fork == nil
# do other stuff...
|
Child pid 31842: terminated |
IO.popen
funciona con un bloque de un modo muy parecido al que lo hace
File.open
.
Al pasar a popen un comando, como podría ser date, el bloque
recibirá un objeto IO como parámetro.
IO.popen ("date") { |f| puts "Date is #{f.gets}" }
|
Date is Sun Jun 9 00:08:50 CDT 2002 |
IO se cerrará automáticamente cuando el código del bloque
termine, tal y como ocurre con
File.open
.
Si asocias un bloque con
Kernel::fork
, el código en
el bloque se ejecutará en un subproceso Ruby, y el padre continuará después del bloque.
fork do
puts "In child, pid = #$$"
exit 99
end
pid = Process.wait
puts "Child terminated, pid = #{pid}, exit code = #{$? >> 8}"
|
In child, pid = 31849 Child terminated, pid = 31849, exit code = 99 |
$? antes de mostrarlo? Ésta es una característica
de los sistemas Posix: los 8 bits menos significativos de un código de salida
contienen la razón por la que el programa terminó, mientras que los 8 más significativos
contienen el código de salida propiamente dicho.