Serializar un objeto es convertirlo en una cadena de bytes con la idea de almacenarlo para su uso posterior o para su uso por otro programa o proceso. Podemos utilizar el módulo Marshal para ello.
Podemos usar marshalling para guardar el estado de un objeto o
:
en un fichero objmsh
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ cat -n marshal2.rb 1 require "Klass" 2 3 o = Klass.new("hello", { :a => 1, :b => 2} ) 4 File.open("objmsh", "wb") do |f| 5 data = Marshal.dump(o, f) 6 endLa clase
Klass
esta definida así:
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ cat -n Klass.rb 1 class Klass 2 def initialize(str, hash) 3 @str, @hash = str, hash 4 end 5 6 def to_s 7 h = @hash.keys.map { |x| "#{x} => #{@hash[x]}" }.join(" ") 8 "str = '#@str' hash = {#{h}}" 9 end 10 endPodemos cargar posteriormente el objeto en la ejecución de un script usando
Marshal.load
.
De este modo es posible guardar el estado de un programa entre ejecuciones:
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ cat -n marshal3.rb 1 require "Klass" 2 3 obj = nil 4 File.open("objmsh","rb") {|f| obj = Marshal.load(f)} 5 puts obj.inspect 6 puts objLa ejecución de
marshal2.rb
serializa el objeto y lo guarda en un fichero:
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ruby marshal2.rb ~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ls -ltr | tail -1 -rw-r--r-- 1 casianorodriguezleon staff 43 13 oct 21:29 objmsh
La ejecución de marshal3.rb
reconstruye el objeto usando Marshal.load
:
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ruby marshal3.rb #<Klass:0x10016a020 @hash={:a=>1, :b=>2}, @str="hello"> str = 'hello' hash = {a => 1 b => 2}
MacBookdeCasiano:distributedRuby casiano$ cat -n fork.rb 1 #!/usr/bin/env ruby -w 2 def do_in_child 3 read, write = IO.pipe 4 5 pid = fork do 6 read.close 7 result = yield 8 write.puts [Marshal.dump(result)].pack("m") 9 exit! 10 end 11 12 write.close 13 puts "In father. PID = #{Process.pid}" 14 result = read.read 15 Process.wait2(pid) 16 r = Marshal.load(result.unpack("m")[0]) 17 puts "#{Process.pid}: #{r.inspect}" 18 end 19 20 do_in_child do 21 puts "In child. PID = #{Process.pid}" 22 { :a => 1, :b => 2 } 23 end
pipe
de IO
crea un canal y retorna los extremos del mismo.
Es necesario que los dos procesos que usan la pareja para comunicarse cierren el extremo
del canal que no usen. El extremo de lectura de un canal no generará un
final de fichero si hay escritores que tienen el canal abierto.
En el caso del padre, el read
no terminaría nunca
si no cierra primero el canal de escritura.
fork
de la clase Process
hace que se cree un proceso hijo.
El fork
retorna al proceso padre el PID
del proceso hijo.
El proceso hijo recibe un nil
.
La sintáxis de fork
es:
fork [{ block }] → fixnum or nil
Si se especifica un bloque - como ocurre en el ejemplo -
es el código de ese bloque el que se ejecuta como subproceso. En tal caso
el fin del bloque indica el final del proceso que por defecto termina con un estatus 0.
Por tanto, en este caso, la llamada a exit!
es un exceso y podría ser eliminada, pero es la forma de
hacer explícita la terminación del proceso.
El proceso padre deberá esperar por la terminación de los procesos hijos usando un wait
o bien no esperar usando un detach
para hacer explícito su desinterés en el estatus del hijo.
La no realización de este protocolo puede conducir a la proliferación de zombies.
Se puede ver si fork
está disponible consultando Process.respond_to?(:fork)
.
pack
de la clase
Array
tiene la sintáxis:
arr.pack ( aTemplateString ) -> aBinaryStringLa llamada empaqueta los contenidos de
arr
en una secuencia
binaria de acuerdo con las directivas especificadas en
aTemplateString
.
Así en la línea:
write.puts [Marshal.dump(result)].pack("m")El formato
"m"
en el parámetro de template indica que la cadena binaria producida por
Marshal.dump(result)
será codificada en Base64.
Base64 denota un grupo de esquemas de codificación que representan datos binarios en una cadena en formato ASCII. Estos esquemas se usan cuando hay necesidad de codificar datos binarios que deben ser almacenados y transferidos sobre un medio que fué diseñado para tratar con datos de tipo texto. De esta forma se asegura que los datos permanecen intactos durante el transporte. Entre otras aplicaciones, es habitual ver el uso de Base64 en el uso de email via MIME y en el almacenamiento de datos complejos en XML.
En este ejemplo el uso de pack
y unpack
parece innecesario ya que el canal está ya en un modo de
transmisión binario de manera que caracteres como el retorno de carro o el tabulador no se interpreten.
Se puede eliminar el uso de pack
y unpack
si se usa syswrite
:
8 write.syswrite Marshal.dump(result)y ahora la lectura y reconstrucción de la estructura queda simplificada:
14 result = read.read 15 Process.wait2(pid) 16 r = Marshal.load(result)
read
de la clase IO
read([length [, buffer]]) => string, buffer, or nil
lee a lo mas length
bytes del stream de I/O, o hasta el final de fichero si se omite o es nil
.
Si se especifica el buffer
, lo leído se guarda en el mismo.
Cuando el método read
alcanza el final de fichero retorna nil
o ""
.
exit!
de la clase Process
termina inmediatamente el proceso hijo.
wait2
hace que el proceso padre espere a la salida del proceso hijo.
El método wait2
retorna un array que contiene el
PID del proceso hijo y su estatus de salida (que es un objeto
Process::Status).
Cuando se ejecuta el programa anterior produce una salida parecida a esta:
MacBookdeCasiano:distributedRuby casiano$ ./fork.rb In father. PID = 6583 In child. PID = 6584 6583: {:a=>1, :b=>2}
$$
?
$?
?
¿Cual será la salida?
(Véase Process::Status).
>> `ls noexiste` ls: noexiste: No such file or directory => "" >> $? => #<Process::Status: pid=1717,exited(1)> >> $?.exitstatus => >> $?.pid => >> $?.signaled? => false >> $?.success? => >> $?.termsig => nil
if (pid = fork) then puts $$ else puts $$ end
1 a = 4 2 if (pid = fork) 3 puts "#{$$}: #{a}" 4 else 5 puts "#{$$}: #{a}" 6 a = 5 7 puts "#{$$}: #{a}" 8 exit! 9 end 10 Process.wait2 11 puts "#{$$}: #{a}"
bc
implementa una calculadora. He aqui un ejemplo de sesión
~/rubytesting/distributedRuby$ bc bc 1.06 Copyright 1991-1994, 1997, 1998, 2000 Free Software Foundation, Inc. This is free software with ABSOLUTELY NO WARRANTY. For details type `warranty'. a = 2*-4 a -8 b = 9+a b 1 ^D ~/rubytesting/distributedRuby$¿Cuál es la salida del siguiente programa? Véase la documentación de popen.
~/rubytesting/distributedRuby$ cat -n bc.rb 1 IO.popen("bc", "r+") do |pipe| 2 pipe.puts("a = 2*-4") 3 pipe.puts("a") 4 output = pipe.gets 5 puts output 6 7 pipe.puts("b = 9+a") 8 pipe.puts("b") 9 pipe.close_write 10 output = pipe.gets 11 puts output 12 end
La librería YAML nos permite cierta interoperabilidad entre lenguajes.
La interoperabilidad entre lenguajes es la posibilidad de que el código interactúe con código escrito en un lenguaje de programación diferente. La interoperabilidad entre lenguajes puede ayudar a maximizar la reutilización de código y, por tanto, puede mejorar la eficacia del proceso de programación.
Aquí tenemos un ejemplo:
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ cat -n marshallingWithYAML.rb 1 require 'YAML' 2 3 fred = {} 4 fred['name'] = "Fred Astair" 5 fred['age'] = 54 6 7 laura = {} 8 laura['name'] = "Laura Higgins" 9 laura['age'] = 45 10 11 test_data = [ fred, laura ] 12 13 puts "The array dumped by Ruby in YAML format:\n"+YAML::dump(test_data) 14 15 IO.popen('perl perlscript.pl', "w+") do |pipe| 16 pipe.puts YAML::dump(test_data) 17 pipe.close_write 18 output = pipe.read 19 puts "The perl script produced this output:\n<<\n#{output}>>" 20 endEste es el código del script Perl arrancado desde Ruby:
/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ cat -n perlscript.pl 1 use strict; 2 use warnings; 3 $| = 1; 4 5 use YAML; 6 7 $/ = undef; 8 my $rubypersons = <STDIN>; 9 my $perlpersons = Load($rubypersons); 10 for (@$perlpersons) { 11 print($_->{name},"\n"); 12 }Este es el resultado de la ejecución:
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ruby marshallingWithYAML.rb The array dumped by Ruby in YAML format: --- - name: Fred Astair age: 54 - name: Laura Higgins age: 45 The perl script produced this output: << Fred Astair Laura Higgins >>
La librería PStore nos permite almacenar y recuperar estructuras de datos en un fichero.
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ cat -n trivialclass.rb 1 class Person 2 attr_accessor :name, :job, :gender, :age 3 end
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ cat -n marshalWithPStore.rb 1 require "trivialclass" 2 require "pstore" 3 4 fred = Person.new 5 fred.name = 'Fred Bloggs' 6 fred.age = 54 7 8 laura = Person.new 9 laura.name = "Laura Higgins" 10 laura.age = 45 11 12 store = PStore.new("persistentobjects") 13 store.transaction do 14 store[:people] ||= [] 15 store[:people] << fred << laura 16 endEl método
transaction(read_only=false)
abre una nueva transacción del almacenamiento de datos.
El código que se pasa como bloque puede leer y escribir datos en el fichero.
El final del bloque produce un commit automático de los cambios.
Es posible producir explícitamente el final de la transacción
llamando a PStore::commit
:
require "pstore" store = PStore.new("data_file.pstore") store.transaction do # begin transaction # load some data into the store... store[:one] = 1 store[:two] = 2 store.commit # end transaction here, committing changes store[:three] = 3 # this change is never reached endtambién es posible finalizar la transacción llamando a
abort
:
require "pstore" store = PStore.new("data_file.pstore") store.transaction do # begin transaction store[:one] = 1 # this change is not applied, see below... store[:two] = 2 # this change is not applied, see below... store.abort # end transaction here, discard all changes store[:three] = 3 # this change is never reached endSi se genera una excepción dentro del bloque se produce una llamada a
abort
.
Si el argumento read_only
es true
,
sólo se podrá acceder para lectura al almacen de datos durante
la transacción y cualquier intento de
cambiarlo producirá una excepción PStore::Error
.
Nótese que PStore no soporta transacciones anidadas.
El siguiente script recupera los datos almacenados:
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ cat -n useMarshalWithPStore.rb 1 require "trivialclass" 2 require "pstore" 3 store = PStore.new("persistentobjects") 4 people = [] 5 store.transaction do 6 people = store[:people] 7 end 8 people.each { |o| 9 puts o.inspect 10 }
Sigue un ejemplo de ejecución:
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ruby marshalWithPStore.rb ~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ls -ltr | tail -1 -rw-r--r-- 1 casianorodriguezleon staff 77 16 oct 12:48 persistentobjects ~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ruby useMarshalWithPStore.rb #<Person:0x10036ee48 @name="Fred Bloggs", @age=54> #<Person:0x10036ed80 @name="Laura Higgins", @age=45>
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ruby marshalWithPStore.rb ~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ls -ltr | tail -1 -rw-r--r-- 1 casianorodriguezleon staff 125 16 oct 12:49 persistentobjectsObtenemos:
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ruby useMarshalWithPStore.rb #<Person:0x10036ee48 @name="Fred Bloggs", @age=54> #<Person:0x10036ed80 @name="Laura Higgins", @age=45> #<Person:0x10036ed08 @name="Fred Bloggs", @age=54> #<Person:0x10036ec90 @name="Laura Higgins", @age=45>¿Cual es la razón?
dump
y load
no funcionan con objetos
de la clase Proc.
Cuando ejecutamos este programa:
~/rubytesting/distributedRuby/serializeProc$ cat -n marshal2.rb 1 o = Proc.new { |a,b | a+b } 2 File.open("objmsh", "wb") do |f| 3 data = Marshal.dump(o, f) 4 endObtenemos un mensaje de error:
~/rubytesting/distributedRuby/serializeProc$ ruby marshal2.rb marshal2.rb:3:in `dump': no marshal_dump is defined for class Proc (TypeError) from marshal2.rb:3 from marshal2.rb:2:in `open' from marshal2.rb:2
Algunos objetos no pueden ser volcados. Si nuestra clase tiene necesidades especiales debemos implementar nuestra propia estrategia de serialización definiendo dos métodos:
_dump
El método de instancia _dump
debe retornar una cadena String
que contenga la información necesaria para la reconstrucción del objeto hasta
la profundidad indicada por un parámetro entero.
Un valor de -1
de dicho parámetro indica que desactivamos la comprobación de profundidad.
_load
El método de clase _load
toma una String
y devuelve el objeto reconstruido.
Veamos un ejemplo de uso de _dump
y _load
.
La clase SerializableProc
permite serializar Procs:
~/rubytesting/distributedRuby/serializeProc$ cat -n SerializableProc.rb 1 class SerializableProc 2 3 def initialize( block ) 4 @block = block 5 @block.sub!(/^/,'lambda ') unless @block =~/^\s*(?:lambda|proc)/ 6 # Test if block is valid. 7 @func = eval "#{@block}" 8 end 9 10 11 def call(* args) 12 @func.call(* args) 13 end 14 15 def arity 16 @func.arity 17 end 18 19 def _dump(limit) 20 @block 21 end 22 23 def self._load(s) 24 self.new(s) 25 end 26 27 endEste código hace uso de la clase:
~/rubytesting/distributedRuby/serializeProc$ cat -n marshalproc2.rb 1 2 require "SerializableProc" 3 4 5 if $0 == __FILE__ 6 7 code = SerializableProc.new %q{ { |a,b| a+b }} 8 9 # Marshal 10 File.open('proc.marshalled', 'w') { |file| Marshal.dump(code, file) } 11 code = File.open('proc.marshalled') { |file| Marshal.load(file) } 12 13 p code.call( 1, 2 ) 14 15 p code.arity 16 17 endSigue una ejecución:
~/rubytesting/distributedRuby/serializeProc$ ruby marshalproc2.rb 3 2
Este otro código muestra otra solución que no hace uso de _dump
y _load
:
~/rubytesting/distributedRuby/serializeProc$ cat -n serializableProc2.rb 1 class SerializableProc 2 3 @@cache = {} 4 def initialize( block ) 5 @block = block 6 @block.sub!(/^/,'lambda ') unless @block =~/^\s*(?:lambda|proc)/ 7 # Test if block is valid. 8 @@cache[@block] = eval "#{@block}" 9 end 10 11 12 def call(* args) 13 @@cache[@block].call(* args) 14 end 15 16 def arity 17 @@cache[@block].arity 18 end 19 20 end 21 22 23 if $0 == __FILE__ 24 25 require 'yaml' 26 require 'pstore' 27 28 code = SerializableProc.new %q{ { |a,b| a+b }} 29 30 # Marshal 31 File.open('proc.marshalled', 'w') { |file| Marshal.dump(code, file) } 32 code = File.open('proc.marshalled') { |file| Marshal.load(file) } 33 34 p code.call( 1, 2 ) 35 36 # PStore 37 store = PStore.new('proc.pstore') 38 store.transaction do 39 store['proc'] = code 40 end 41 store.transaction do 42 code = store['proc'] 43 end 44 45 p code.call( 1, 2 ) 46 47 # YAML 48 File.open('proc.yaml', 'w') { |file| YAML.dump(code, file) } 49 code = File.open('proc.yaml') { |file| YAML.load(file) } 50 51 p code.call( 1, 2 ) 52 53 p code.arity 54 55 endLa solución "memoiza" en un hash que es un atributo de la clase la relación entre el fuente y el código. Cuando se ejecuta el código anterior se obtiene:
~/rubytesting/distributedRuby/serializeProc$ ruby serializableProc2.rb 3 3 3 2
prun
que permita lanzar un número dado de procesos:
res = prun( :one => lambda { |n| "#{n} is 1" }, :three => lambda { |n| "#{n} is 3" }, 'two' => lambda { |n| n*4 } ) puts res.inspect res = prun( [2, 3, 7] => lambda { |n| n[0]+n[1]+n[2] }, [4, 5] => lambda { |n| n[0]*n[1] } ) puts res.inspectEste programa debería producir una salida similar a esta:
~/rubytesting/distributedRuby$ ruby parfork2.rb {"two"=>"twotwotwotwo", :one=>"one is 1", :three=>"three is 3"} {[2, 3, 7]=>12, [4, 5]=>20}
res
devuelto por prun
será un hash cuyas claves son las mismas que se han pasado como argumentos
y cuyos valores contienen los resultados devueltos por las correspondientes lambdas.
wait2
contiene el PID del hijo. Esta información puede ser usada para
que el proceso padre determine que canal usar para la lectura
Casiano Rodriguez León 2015-01-07