2011-01-04 19 views
6

A menudo veo el uso y la explicación de las estrategias paralelas de Haskell conectadas con cálculos puros (por ejemplo fib). Sin embargo, no suelo ver que se use con construcciones monádicas: ¿hay una interpretación razonable del efecto de par y funciones relacionadas cuando se aplica a ST s o IO? ¿Se ganaría velocidad con tal uso?Uso de estrategias paralelas con mónadas

+0

Desea que sus acciones de IO se realicen en un orden determinado (por ejemplo, primero abra el archivo, luego de leerlo, luego ciérrelo). ¿Qué quieres paralelizar allí? – helium

+1

@helium: esto parece surgir con datos mutables o al usar el FFI. –

+0

Me lo he preguntado. A menudo abro varios archivos grandes (100 megas) y los descomprimo en hilos paralelos y luego trabajo con ellos después de que están abiertos. Son lo suficientemente grandes como para ver una buena mejora en el rendimiento, pero lo suficientemente pequeños como para mantenerlos en la memoria. Me he preguntado cómo hacer esto en Haskel. –

Respuesta

12

El paralelismo en la mónada IO se denomina más correctamente "Concurrencia", y es compatible con forkIO y amigos en el módulo Control.Concurrent.

La dificultad de la paralelización de la mónada ST es que ST es necesariamente de un solo hilo: ese es su objetivo. Existe una variante diferida de la mónada ST, Control.Monad.ST.Lazy, que en principio podría admitir una evaluación paralela, pero no conozco a nadie que haya intentado hacer esto.

Hay una nueva mónada para evaluación paralela llamada Eval, que se puede encontrar en las versiones más recientes de parallel package. Recomiendo utilizar la mónada Eval con rpar y rseq en lugar de par y pseq en estos días, porque conduce a un código más robusto y legible. Por ejemplo, el habitual fib ejemplo, se puede escribir

fib n = if n < 2 then 1 else 
     runEval $ do 
      x <- rpar (fib (n-1)) 
      y <- rseq (fib (n-2)) 
      return (x+y) 
1

Hay algunas situaciones en las que esto tiene sentido, pero en general no debería hacerlo. Se comprobarán los siguientes:

doPar = 
    let a = unsafePerformIO $ someIOCalc 1 
     b = unsafePerformIO $ someIOCalc 2 
    in a `par` b `pseq` a+b 

en doPar, un cálculo para a se generó, a continuación, el hilo principal evalúa b. Pero, es posible que después de que el hilo principal termine el cálculo de b comenzará a evaluar a también. Ahora tiene dos hilos que evalúan a, lo que significa que algunas de las acciones IO se realizarán dos veces (o posiblemente más). Pero si un hilo termina de evaluar a, el otro solo dejará caer lo que se ha hecho hasta ahora. Para que esto sea seguro, necesita algunas cosas para ser verdad:

  1. Es seguro que las acciones de IO se realicen varias veces.
  2. Es seguro que solo se realicen algunas de las acciones de E/S (por ejemplo, no hay limpieza)
  3. Las acciones de E/S están libres de cualquier condición de carrera. Si un hilo muta algunos datos al evaluar a, ¿el otro hilo que también funciona en a se comportará con sensatez? Probablemente no.
  4. Cualquier llamada extranjeros son re-entrantes (que necesita esto para la concurrencia, en general, por supuesto)

Si su someIOCalc se parece a esto

someIOCalc n = do 
    prelaunchMissiles 
    threadDelay n 
    launchMissiles 

que es absolutamente no es seguro de usar esto con par y unsafePerformIO.

Ahora, ¿alguna vez lo vale? Tal vez. Las chispas son baratas, incluso más baratas que los hilos, por lo que en teoría debería ser una ganancia de rendimiento. En la práctica, quizás no tanto. Roman Leschinsky tiene un buen blog post about this.

Personalmente, he encontrado que es mucho más simple razonar sobre forkIO.