cerrar-sesion editar-perfil marker video calendario monitor periodico fax rss twitter facebook google-plus linkedin alarma circulo-derecha abajo derecha izquierda mover-vertical candado usuario email lupa exito mapa email2 telefono etiqueta

400510301. Python NetWorkSpaces y programas paralelos

Escrito por Redacción en Secciones
no hay comentarios Haz tu comentario
Imagen de logotipo de facebook Imagen de logotipo de Twitter Imagen de Logotipo de Google+ Imagen de logotipo de Linkedin

La programación paralela tiene fama de ser un campo exótico, cultivado por expertos que se sirven de máquinas tremendamente grandes y caras. Por desgracia, debido en parte a su historia, los lenguajes y herramientas de programación paralela siguen concentrándose fundamentalmente en “grandes máquinas” y en lenguajes antiguos como C y Fortran.

La mejora de la funcionalidad mediante el paralelismo debería ser de interés para todos aquellos cuyo código se ejecuta demasiado lento. Eso requiere un cambio de enfoque.

En la actualidad, muchos ordenadores nuevos tienen núcleo múltiple y la mayoría de los usuarios tienen acceso a múltiples ordenadores. Muchos desarrolladores trabajan con lenguajes dinámicos más nuevos como Python y R. Para dar respuesta a las necesidades de estos usuarios hemos desarrollado un sistema de coordinación basado en Python y denominado “NetWorkSpaces” (NWS) que es fácil de aprender, accesible mediante la mayoría de los entornos de desarrollo (incluidos R, Java, Octave, Python, Perl, y Ruby), y se puede instalar en colecciones ad hoc de CPU que nos sobren.

((En la actualidad, muchos ordenadores nuevos tienen núcleo múltiple y la mayoría de los usuarios tienen acceso a múltiples ordenadores))

Pero aunque su simplicidad lo convierte en una buena opción para ejemplos pedagógicos, no es un sistema de juguete. Hemos utilizado NWS para que ejecute programas paralelos en cientos de procesadores, lo que produce muchos años CPU de muy útil computación.

NetWorkSpaces

NetWorkSpaces (www.lindaspaces.com/products/NWS_overview.html) se ha desarrollado en Scientific Computing Associates y se encuentra disponible en SourceForge (nws-py.sourceforge.net).

Hay que instalar tanto el servidor (en una máquina) como un cliente (en todas las máquinas implicadas en la computación). El servidor se implementa mediante Python y Twisted, que son necesarios. Aunque NWS se implementa en Python, tenemos un API de cliente NetWorkSpace para una serie de lenguajes. Si bien aquí describimos el cliente Python, las ideas se aplican también a otros clientes de lenguajes.

NWS se basa en el concepto de un conjunto de enlaces, que en los lenguajes de programación a veces se conocen por “environments,” “namespaces,” “workspaces,” y cosas por el estilo. Generalmente en los lenguajes de programación, un enlace mapea un nombre a un valor. Como este es un concepto familiar para los programadores, es una buena base para construir un sistema de coordinación.

Un lenguaje dado tiene normas sobre nombres permisibles, valores permisibles para un nombre dado, y el contexto en el que el enlace es válido (el ámbito del enlace). El lenguaje también ofrece operaciones para establecer un enlace y para extraer el valor de un nombre enlazado.

A menudo estas operaciones vienen implícitas en la estructura léxica del código, como también lo es el conjunto de enlaces que se pretende. Así, por ejemplo, en x = y, la y implica una búsqueda del valor ligado a y, mientras que x es el objetivo de la asignación. Las normas del ámbito determinan a qué x y a qué y nos referimos.

NWS ofrece una encapsulación particular de semántica de enlace.

Mediante esta encapsulación, especificamos de forma explícita la búsqueda (fetch), la asociación del nombre x con el valor extraído (store), y el conjunto de enlace que se persigue (indicado por el objeto ws). Así, una simple asignación tiene este aspecto en NWS:

ws.store(‘x’, ws.fetch(‘y’))

Hasta aquí, hemos logrado hacer un constructor de rutinas algo más verboso. El punto clave es que la encapsulación NWS se aviene a una implementación basada en red, que permite que distintos procesos intercambien datos y se sincronicen mediante enlaces NWS.

En muchos lenguajes, incluido Python, podríamos haber usado una sintaxis similar a la de los enlaces normales:

ws.x = ws.y

Sin embargo, la semántica de estas variables NWS difieren en cosas importantes de las variables normales. En nuestra opinión, es erróneo crear la falsa ilusión de la similitud cuando, de hecho, hay diferencias importantes.

NWS está diseñado para ser un servicio de coordinación que no se inclina a favor de ningún lenguaje concreto. Las ventajas que esta neutralidad ofrece son las siguientes:
– Los patrones y expresiones idiomáticas de coordinación NWS pueden ser recicladas de un entorno de lenguaje al siguiente.
– Puede usarse NWS para coordinar conjuntos heterogéneos de código escrito en lenguajes distintos.

((El punto clave es que la encapsulación NWS se aviene a una implementación basada en red, que permite que distintos procesos intercambien datos))

Para facilitar la coordinación entre lenguajes, los nombres de variables NWS son cadenas ASCII y no necesitan atenerse a las normas de denominación de variables de un lenguaje dado. Los valores pueden ser cualquier tipo nativo en el lenguaje cliente para el que ese lenguaje tiene una serialización en la que se puede trabajar.

La mayoría de los valores en la mayor parte de los lenguajes mencionados pude ser serializada de manera automática (la serialización la hace NWS entre bastidores, y no es preocupación directa de los programadores)
Por ejemplo, Python NWS puede gestionar automáticamente estructuras de datos mixtos:

>>> from nws.client import NetWorkSpace

>>> ws=NetWorkSpace(‘test’)

>>> l=(‘a’,’b’,’c’)

>>> t=(,2,3)

>>> d=‘list’:l, ‘tuple’:t

>>> ws.store(‘dict example’, d)

>>> ws.fetch(‘dict example’)

‘list’: (‘a’, ‘b’, ‘c’), ‘tuple’: (, 2, 3)

Por último, las cadenas ASCII usadas como valores son tratadas de una forma especial (no están sujetas al protocolo de serialización de lenguajes cliente) que hace posible que sean intercambiadas entre lenguajes cliente. En el Ejemplo 1, por ejemplo, podemos usar NWS para trasladar datos de Python a R codificados como una cadena ASCII.

Comportamiento de enlace coordinado

(
Python

>>> from nws.client import NetWorkSpace

>>> ws=NetWorkSpace(‘tickets’)

>>> ws.store(‘ticket’, ‘ticket string’)

R
> library(nws)

> ws<-netWorkSpace('tickets') > nwsFetch(ws, ‘ticket’)

(1) «ticket string»

Ejemplo 1. Empleo de NWS para trasladar datos de Python a R codificados como una cadena ASCII.

)]

Si examinamos de nuevo x=y, ¿qué pasaría si y no hubiera sido definido todavía? Con los programas de secuencia convencionales, podríamos obtener datos basura. Un lenguaje que nos sirviera de más ayuda podría indicar un error o hacer surgir una excepción, después de todo, nadie más va a venir a definir y por nosotros.

Pero en una configuración de conjuntos, alguien podría hacerlo. En otras palabras, en el contexto de la coordinación, un nombre no enlazado tiene una interpretación perfectamente válida (y útil): “Por favor espere”. Consideremos ahora:

x = 123

x = 456

En la configuración convencional de programas secuenciales, sería raro ver ese tipo de código (y un compilador podría avisar sobre el código muerto o eliminarlo), pero es razonable dejar que la segunda asignación simplemente sobre-escriba el 123 con un 456. Somos el único proceso que hay, así que ¿a quién más le va a importar el antiguo valor?
Pero en las configuraciones de conjunto, otros procesos pueden estar interesados en la secuencia de valores ligados a x. Si es así, ¿cómo sabemos que se ha puesto en buen uso a un valor concreto de x?
Entremos en la comunicación generativa. Algunos eventos de coordinación generan datos que existen independientes de cualquier proceso, otros consumen esos datos.

Interpretamos el ligado de variables como añadir un valor a una lista de valores, en vez de sobre escribir un único valor. Lo hacemos manteniendo una cola FIFO de valores. Pero ¿cómo esparcimos los valores? Para tener una visión completa, la extracción de un valor ligado a un nombre elimina un valor de la cola.

En resumen, una asignación registra un valor de interés, una recuperación consume un valor, y un listado vacío de valores desencadena un “Por favor espere” para una recuperación. Para ver lo bien que actúan juntos en una sesión de Python, podemos ejecutar el siguiente código “obrero”:

from nws.client import NetWorkSpace

def f(x): return x*x*x

ws = NetWorkSpace(‘table’)

while True:

ws.store(‘r’, f(ws.fetch(‘x’)))

A continuación, en otra sesión de Python, ejecutamos el “patrono”:

ws = NetWorkSpace(‘table’)

for x in range(100): ws.store(‘x’, x)

for x in range(100): print ‘f(%d) = %d’%(x, ws.fetch(‘r’))

Veremos impreso un listado de números y sus cubos, computados por el obrero:
– El patrono y el obrero pueden ser iniciados en cualquier orden.
– El número de obreros no se especifica en el código patrono y es totalmente flexible, pero se necesitaría un modesto cambio de código para imprimir los resultados en el orden correcto si se utiliza más de un obrero.

Otros patrones de coordinación

Supongamos que los procesos están cooperando en la búsqueda de un valor, xmax, que maximiza una función F; es decir, F(xmax) es el valor más alto que logra F. Cada proceso tiene su propio listado de valores x a comprobar. Querríamos hacer algo como esto:

for x in MyCandidateList:

currentMax = ws.fetch(‘max’)

y = f(x)

if y > currentMax: ws.store(‘max’, y)

Pero eso sería erróneo, ya que fetch consume un valor que puede no ser reemplazado.

Querríamos una forma de consultar una variable sin destruir el valor. En NWS, find retorna un valor sin destruirlo. Así pues, podríamos intentar lo siguiente:

for x in MyCandidateList:

currentMax = ws.find(‘max’)

y = f(x)

if y > currentMax: ws.store(‘max’, y)

Pero sería erróneo también —ya no estamos manteniendo un único valor máximo, y currentMax podría no ser realmente actual. ¿Y si hacemos lo siguiente?:

for x in MyCandidateList:

currentMax = ws.find(‘max’)

y = f(x)

if y > currentMax:

currentMax = ws.fetch(‘max’)

if y > currentMax: currentMax = y

ws.store(‘max’, currentMax)

Ahora sólo actualizamos max cuando currentMax es realmente más grande. Como está el par fetch/store, sólo tenemos un valor asociado a max (este par de operaciones también garantiza la atomicidad de la actualización). Pero supongamos que es poco probable que un x dado resulte en un nuevo máximo.

Podríamos reducir aún más el tráfico de coordinación invocando find sólo en iteraciones alternas, o cada diez, etc. Como siempre consultamos el auténtico valor actual de max antes de comprometer un cambio, los valores pasados no nos llevarán a resultados incorrectos. Pero sí incrementarán el número de intentos de actualización fallidos.

((Ahora sólo actualizamos max cuando currentMax es realmente más grande. Como está el par fetch/store, sólo tenemos un valor asociado a max))

Find tiene otros usos, y entre los más corrientes están las variables de “write-once” como los datos de inicialización que se establecen al principio de una computación y que los necesitan dos o más miembros del conjunto. Find altera la forma en que se referencian las colas de valores. ¿Y qué pasa con las variaciones de la cola misma? NWS da soporte a cuatro modos para valores:
– “El primero que entra, sale el primero ” (por defecto)
– “El último que entra, sale el primero”
– No determinista (no aleatorio, sólo arbitrario).
– Único (la variable sólo contiene un valor; store sobre escribe el valor anterior, si lo hay)
El modo único funciona bien con la operación find y es útil para los valores de estatus; podemos actualizar el estatus sin preocuparnos de extraer (fetch) el estatus anterior.

from nws.client import SINGLE

ws.declare(‘status’, SINGLE)

while True:

time.sleep(60)

status=…

ws.store(‘status’, status)

Iteradores NWS

Es también posible iterar mediante valores en NWS. Ifind retorna un iterador que retorna cada valor ligado a la variable especificada:

for v in range(5): ws.store(‘v’, v)

for v in ws.ifind(‘v’): print v

Si probamos esta forma, hemos de notar que el segundo bucle cuelga después de imprimir cinco valores. Ello es porque ifind está esperando más valores. Si usamos otro proceso para almacenar más valores a v, se despierta y los imprime. Para indicarle que pare, podemos borrar el vínculo:

ws.deleteVar(‘v’)

Otra opción es utilizar ifindTry, que crea un iterador que finaliza en cuanto no hay más valores. Por último, ifetch y ifetchTry son métodos similares que consumen valores según van iterando. Los iteradores NWS sólo se pueden usar en variables FIFO o de modo Único.

Gestión de espacios de trabajo

NWS soporta múltiples espacios de trabajo para ayudar a organizar las variables en grupos relacionados Hemos visto cómo el constructor toma el nombre de un espacio de trabajo como primer argumento.

También podría tomar un nombre de anfitrión y un número de puerto para especificar al servidor que aloja al espacio de trabajo. (Algo así tenía que llegar. Sin ello ¿cómo podríamos formar conjuntos con partes que se ejecutan en computadoras que son diferentes?) Así, si cada uno de los procesos múltiples ejecuta:

ws = NetWorkSpace(‘example’, serverHost=’myhost.mycorp.com’)

todos tendrían acceso al mismo espacio de trabajo, que es gestionado por el servidor NWS accesible mediante un puerto por defecto en myhost.

¿Pero quién sería el propietario? ¿Y por qué tendría que importar? Por defecto, el proceso que primero menciona el espacio de trabajo al servidor es el propietario. Esto sí importa porque al cabo de un tiempo tendremos que limpiar.

Los espacios de trabajo y los datos que hay en ellos pueden ser persistentes, lo que significa que existen hasta ser destruidos de forma explícita. En la práctica, esto puede llevar a un buen lío, así que son, por defecto, transitorios cuando el proceso propietario sale, son destruidos.

((NetWorkSpaces y las variables que contienen existen independientemente de programas cliente particulares))

La persistencia es útil en ciertas configuraciones y está disponible mediante una opción para el constructor del espacio de trabajo. Normalmente, la política de propiedad por defecto hace lo correcto; un proceso patrono menciona NWS el primero y se convierte en propietario.

A continuación otros procesos que lo utilizan pueden ir y venir sin destruir el NWS. Por último, cuando sale el patrono, se limpia el NWS. Pero a veces deseamos poder mencionar un NWS sin convertirnos en propietarios de forma inadvertida, como podría ocurrir si un proceso auxiliar vence al patrono a la primera mención. Si se declara el NWS con useUse true ello indica que el proceso no quiere ser propietario de NWS:

ws = NetWorkSpace(‘not mine’, useUse=True)

La denominación de espacios de trabajo presenta algún pequeño reto: ¿Cómo podemos evitar las colisiones de nombres? Consideremos, por ejemplo, instancias múltiples de una aplicación de conjunto que use el mismo servidor NWS. Cada objeto de espacio de trabajo NWS lleva asociado un objeto de servidor (encapsulando la conexión al servidor que gestiona el espacio de trabajo).

Este objeto proporciona un método mktempWs que funciona algo así como el mkdtemp de Linux. Retorna una referencia a un espacio de trabajo cuyo nombre se garantiza que es único en el servidor. Una vez tenemos nuestro propio espacio de trabajo, podemos usar nombres de variables dentro del mismo, sin tener que preocuparnos de los que colisionan con algún otro programa de conjunto.

La interfaz web

NetWorkSpaces y las variables que contienen existen independientemente de programas cliente particulares. Puede resultar útil examinar los contenidos cuando se está depurando o comprendiendo un programa, o controlando el progreso de una aplicación. NWS incluye una interfaz web que muestra los espacios de trabajo y sus contenidos.

La Figura es un listado de NetWorkSpaces; si hemos pinchado sobre “test,” veremos algo así como lo que aparece en la Figura 2. Al pinchar en una variable, podemos ver un listado de los valores actuales (Figura 3)

Como los valores son objetos serializados que a Python le resulta difíciles de interpretar, ¿cómo puede la interfaz de la web mostrar valores que proceden de otros lenguajes (vectores R, matrices octavas, etc.)?

La traducción la realizan unos servicios “babelfish” específicos de lenguajes que saben cómo deserializar y mostrar sus propios tipos de datos. Babelfish son sólo clientes normales que tienen tareas de traducción y que devuelven los resultados a través de NWS.

Como podemos ver, la interfaz ofrece una capacidad limitada para alterar el estado NWS al borrar espacios de trabajo y variables. Un código diseñado de la forma apropiada puede hace un buen uso de ello como mecanismo de señalización. Lo usamos, por ejemplo, para ofrecer una inicialización de procesos con la web como intermediario para ciertos despliegues del sistema Sleigh (trineo).

Sleigh

Hasta aquí hemos dado por sentados los procesos de coordinación; no hemos descrito cómo se crean y se controlan. Podríamos hacer uso de un mecanismo externo para lanzar y gestionar los procesos pero, porque es una cuestión tan importante, NWS incluye el suyo propio con Sleigh. Sleigh ofrece además funcionalidad para gestionar directamente ciertos tipos de problemas de estilo patrono/obrero “embarazosamente paralelos”, ahorrándoles a los usuarios la necesidad de meterse directamente con NWS.

Sleigh lanza procesos “obreros” genéricos en máquinas que quedan a la espera para realizar trabajos. Aquí tenemos el escenario más simple:

>>> from nws.sleigh import Sleigh

>>> s = Sleigh()

Por defecto, Sleigh crea tres obreros. Vamos a ver dónde se están ejecutando realmente los procesos, preguntando a cada obrero que está tirando del trineo por su nombre anfitrión:

>>> from socket import gethostname

>>> s.eachWorker(gethostname)

(‘newt’, ‘newt’, ‘newt’)

El método eachWorker de Sleigh ejecuta la función específica una vez en cada proceso obrero, y retorna los valores en un listado. Podemos ver que, por defecto, Sleigh inicia tres procesos obrero en la máquina local. (newt). Ejecutar a los obreros en la máquina local resulta útil para la comprobación o, si resulta que tenemos un ordenador de núcleo múltiple pero también queremos poder usar otras máquinas. Así pues, vamos a cerrar esto, y a probar algo diferente:

>>> s.stop()

>>> from nws.sleigh import sshcmd

>>> s = Sleigh(nodeList=(‘hippo’, ‘newt’, ‘python’, ‘rhino’), launch=sshcmd)

>>> s.eachWorker(gethostname)

(‘rhino’, ‘hippo’, ‘newt’, ‘python’)

Este ejemplo asume que los usuarios han configurado correctamente ssh para permitir poder entrar sin contraseña a los ordenadores que están en el listado de nodos.

eachWorker se utiliza normalmente para inicializar obreros (por ejemplo, importando paquetes o cargando datos comunes de un fichero), pero el trabajo real en Sleigh lo hace normalmente eachElem. Este método toma como input una función y una listado, y retorna un listado con los resultados de aplicar la función a cada elemento del listado de input.

El número de tareas en el listado no tiene por qué ser igual al número de obreros. Normalmente hay más tareas que obreros, y los obreros cooperan para computar todas las tareas. Usamos eachElem par computar la misma tabla de cubos como antes:


>>> r = s.eachElem(lambda x: x*x*x, range(100))

>>> len(r)

100

>>> r(2:5)

(8, 27, 64)

Podemos ver que los resultados se retornan en orden. En contraste con el programa original, Sleigh se encarga de los detalles relacionados con el reparto de tareas, la recolección de resultados y poner en marcha y detener a los obreros. eachElem puede gestionar las funciones generales con parámetros múltiples fijos y variables. Por ejemplo:


s.eachElem(f, ([1,2,3),(11,12,13)],(99))

invocaría a f(1,11,99), f(2,12,99), y f(3,13,99). Esta funcionalidad (que también se extiende a permutaciones del listado de argumentos) puede acomodar una variedad de prototipos de función sin necesidad de escribir códigos envoltorio o volver a trabajar las funciones mismas.

((Cada tarea se despierta en un obrero genérico, cualquiera que esté libre en ese momento. Al obrero se le dan las definiciones del módulo que contiene la función en la que está trabajando eachElem))

Cada tarea se despierta en un obrero genérico, cualquiera que esté libre en ese momento. Al obrero se le dan las definiciones del módulo que contiene la función en la que está trabajando eachElem, además de cualquier definición construida por invocaciones anteriores de eachWorker (en el ejemplo, hemos usado eachWorker para inicializar algunas variables globales).

Ello implica que los obreros mantienen su estado en diferentes tareas, lo que puede resultar útil, pero también una fuente potencial de fallos técnicos.

Por defecto, eachElem está bloqueando es decir, la invocación no va a retornar hasta que hayan retornado todos los resultados de los obreros. Como opción, se puede invocar eachElem en modo asíncrono en el que inmediatamente retorna no el listado de resultados sino un objeto SleighPending. El objeto incluye métodos para consultar el progreso de la computación y para obtener los resultados finales.

Su montaje

Ahora vamos a ponerlo todo junto en un programa semi-realista que calcula los primos hasta, pero sin incluir,, N. Hay casi tantos enfoques diferentes para encontrar los buscadores paralelos de primos como hay primos. En este, hemos usado eachElem para crear un número de tareas, en el que cada tarea representa un rango de enteros impares que deberían ser peinados en busca de primos.

Para comprobar la cualidad de primo, vamos a intentar dividir cada número, n, entre todos los primos menores o iguales que la ­(n), (en inglés sqrt(n)) lo que implica que las tareas dependen de los resultados de tareas anteriores.

[(
Listado 1

import sys

from nws.sleigh import Sleigh

def initPrimes():

global chunk, chunks, limit

limit, chunk = SleighUserNws.find('prime parameters')

chunks =

def findPrimes(first):

last = min(first+chunk, limit)

# we need to know all the primes up to the smaller of the start of

# this chunk or the square root of its last element.

need = min(first-2, int(.5+(last-1)**.5))

myPrimes = ()

for c in xrange(3, need+1, chunk):

if not c in chunks: chunks(c) = SleighUserNws.find('primes%d'%c)

myPrimes += chunks(c)

newx = len(myPrimes)

for x in xrange(first, last, 2):

for p in myPrimes:

if x%p == 0: break

else: myPrimes.append(x)

if first**2 < limit: SleighUserNws.store('primes%d'%first, myPrimes(newx:))

return myPrimes(newx:)

def master(workers, limit, chunk):

s = Sleigh(workerCount=workers)

s.userNws.store('prime parameters', (limit, chunk))

s.eachWorker(initPrimes)

primes = (2)

map(primes.extend, s.eachElem(findPrimes, range(3, limit, chunk)))

return primes

if __name__=='__main__':

primes = master(int(sys.argv(1)), int(sys.argv(2)), int(sys.argv(3)))

print len(primes), primes(:3), '...', primes[-3:]

)]

El Listado número 1 es el código entero. El patrono instancia un Sleigh, inicializa a sus obreros mediante eachWorker, crea un listado de tareas, y después ejecuta las tareas mediante eachElem. Los dos parámetros generales que dan la longitud del trozo (que tiene que ser par), y N, están ligados a una variable en un NWS y son extraídos por la función invocada mediante eachWorker.

Cada tarea está representada por un entero: el comienzo de un trozo de enteros para comprobar la cualidad de primo. Cada tarea retorna un listado de primos que se encontraron en ese trozo. Para comprobar un número en particular puede que se necesiten candidatos de factor primo que el obrero no conoce todavía, en cuyo caso los obtendrán mediante find.


Los obreros usan SleighUserNws como su NWS; este es un objeto de espacio de trabajo que corresponde a un NWS denominado de forma exclusiva y creado por Sleigh. Es un lugar muy conveniente para almacenar variables relacionadas con la ejecución de Sleigh. Además, los obreros recuerdan qué primos han visto de manera que no tienen que obtenerlos para tareas posteriores.

Conclusión

Python y NetWorkSpaces facilitan la creación y experimentación con programas paralelos sin necesitar de herramientas o hardware especializados. La comunicación y la sincronización se ven enormemente simplificadas con la reducción a la manipulación de variables en un espacio de trabajo compartido.

Los programas pueden ser comprobados en una única CPU mediante múltiples procesos (o hilos), o para ver la aceleración real, en CPU de núcleo múltiple o en una colección de ordenadores. El estado de la computación paralela es captado por las variables almacenadas en NetWorkSpace.

Estas pueden visualizarse mediante la interfaz de web, que facilita la comprensión y el depurado del programa, y controlan el progreso realizado. Como NetWorkSpaces es neutral y no se inclina por ningún lenguaje, se puede transferir código y expresiones idiomáticas a distintos entornos, e incluso se puede utilizar para crear conjuntos de programas escritos en lenguajes distintos.

Quiero agradecer a Scientific Computing Associates ([www.lindaspaces.com ) por apoyar el desarrollo de NetWorkSpaces, y a Twisted Matrix Labs por apoyar Twisted (www.twistedmatrix.com ). Sleigh se inspiró en parte en el paquete SNOW (abreviatura de "Simple Network Of Workstations") de R Project, desarrollado por Luke Tierney, A.J. Rossini, Na Li, y H. Sevcikova (cran.rproject.org/doc/packages/snow.pdf). DDJ

Etiquetas

Noticias relacionadas

Comentarios

No hay comentarios.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

Debes haber iniciado sesión para comentar una noticia.