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 end
La 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 end
Podemos 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 obj
La 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 end
Este 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 end
El 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
end
tambié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
end
Si 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 end
Obtenemos 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 end
Este 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 end
Sigue 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 end
La 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
escribiendo un método 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.inspect
Este 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