2011-01-04 2 views
6

Je vois souvent l'utilisation et l'explication des stratégies parallèles de Haskell connectées à des calculs purs (par exemple fib). Cependant, je ne le vois pas souvent avec des constructions monadiques: y a-t-il une interprétation raisonnable de l'effet de par et des fonctions associées lorsqu'il est appliqué à ST s ou IO? Y aurait-il une accélération de cette utilisation?Utilisation de stratégies parallèles avec des monades

+0

Vous souhaitez que vos actions E/S soient exécutées dans un certain ordre (par exemple, ouvrez d'abord le fichier, puis lisez-le plutôt que de le fermer). Que voulez-vous paralléliser ici? – helium

+1

@helium: cela semble principalement provenir de données mutables ou lors de l'utilisation du FFI. –

+0

Je me suis demandé cela. J'ouvre souvent plusieurs fichiers volumineux (100 Mo) et les décompresser dans des threads parallèles, puis travailler avec eux après leur ouverture. Ils sont assez grands pour voir une amélioration des performances, mais assez petit, je peux les garder en mémoire. Je me suis demandé comment faire ça à Haskel. –

Répondre

12

Le parallélisme dans la mona IO s'appelle plus correctement "Concurrency" et est pris en charge par forkIO et les amis dans le module Control.Concurrent.

La difficulté avec la parallélisation de la monade ST est que ST est nécessairement monotrou - c'est son but. Il y a une variante paresseuse de la monade ST, Control.Monad.ST.Lazy, qui en principe pourrait supporter l'évaluation parallèle, mais je ne suis au courant de personne ayant essayé de le faire.

Il existe une nouvelle monade pour l'évaluation parallèle appelée Eval, qui peut être trouvée dans les versions récentes du parallel package. Je recommande d'utiliser la monade Eval avec rpar et rseq au lieu de par et pseq ces jours-ci, car il conduit à un code plus robuste et lisible. Par exemple, le fib exemple usuel peut être écrit

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

Il y a des situations où cela a du sens, mais en général vous ne devriez pas le faire. Examinez les éléments suivants:

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

dans doPar, un calcul pour a est déclenché, le thread principal évalue b. Mais, il est possible qu'après que le thread principal termine le calcul de b, il commencera également à évaluer a. Vous avez maintenant deux threads évaluant a, ce qui signifie que certaines des actions d'E/S seront effectuées deux fois (voire plus). Mais si un thread finit d'évaluer a, l'autre va simplement laisser tomber ce qu'il a fait jusqu'à présent. Pour que cela soit sûr, vous devez avoir certaines choses pour être vraies:

  1. Il est possible d'exécuter plusieurs fois les actions d'E/S.
  2. Il est sûr que seules certaines des actions d'E/S sont effectuées (par exemple, il n'y a pas de nettoyage)
  3. Les actions E/S sont exemptes de conditions de concurrence. Si un thread mute certaines données lors de l'évaluation a, l'autre thread travaillant également sur a se comporte-t-il raisonnablement? Probablement pas.
  4. Tous les appels étrangers sont réentrantes (vous en avez besoin pour la concurrence en général bien sûr)

Si votre someIOCalc ressemble à ceci

someIOCalc n = do 
    prelaunchMissiles 
    threadDelay n 
    launchMissiles 

il est absolument pas sûr d'utiliser avec par et unsafePerformIO.

Maintenant, cela en vaut-il la peine? Peut être. Les étincelles sont bon marché, même moins chères que les threads, donc en théorie cela devrait être un gain de performance. En pratique, peut-être pas tellement. Roman Leschinsky a une belle blog post about this.

Personnellement, j'ai trouvé beaucoup plus simple de raisonner sur forkIO.

Questions connexes