HAL Id: hal-00665918
https://hal.inria.fr/hal-00665918
Submitted on 3 Feb 2012
HAL
is a multi-disciplinary open access archive for the deposit and dissemination of sci- entific research documents, whether they are pub- lished or not. The documents may come from teaching and research institutions in France or abroad, or from public or private research centers.
L’archive ouverte pluridisciplinaire
HAL, estdestinée au dépôt et à la diffusion de documents scientifiques de niveau recherche, publiés ou non, émanant des établissements d’enseignement et de recherche français ou étrangers, des laboratoires publics ou privés.
Christophe Deleuze
To cite this version:
Christophe Deleuze. Concurrence et continuations en OCaml. JFLA - Journées Francophones des
Langages Applicatifs - 2012, Feb 2012, Carnac, France. �hal-00665918�
Conurrene et ontinuations en OCaml
C. Deleuze
Laboratoirede Coneption etd'IntégrationdesSystèmes
50 rueBarthélémy de Laemas
26902ValeneCedex 09
Frane
hristophe.deleuzelis.grenoble-inp.fr
Résumé
Nous dérivonsetmettonsen÷uvrediérentesméthodespourfournir laonurrene légère
en OCaml, en style diret ou indiret. Nous montrons que toutes es approhes font appel,
expliitement ou impliitement, à la notion de ontinuation. Nous essayons de préiser leurs
relations et omparons leur performanes surdeux appliations onurrentes simples. Presque
touteslesmisesen÷uvrelégèressont trèslargementmeilleuresquelesthreadssystèmeoudela
mahinevirtuelle.
1. Introdution
On peut dénir la onurrene omme une propriété d'un système dans lequel plusieurs ls
d'éxéution existent et progressent simultanément. Ces threads représentent des traitements entre
lesquels il n'y a pas de dépendane temporelle par défaut : les dépendanes sont exprimées
expliitementpardesopérationsdesynhronisation.
Lessystèmesd'exploitationfournissentetteabstrationmaisaveunoûtrelativementimportant,
en mémoire utilisée par thread et en temps pour hanger de ontexte. L'ordonnanement est
généralementpréemptifequirenddéliatlepartagededonnéesentrelesthreads.
Il est possible d'obtenir une onurrene plus légère en la mettant en ÷uvre au niveau de
l'appliation. De nombreuses approhes ont été proposées ave généralement un ordonnanement
oopératif. Dans et artile nous montrons que toutes es approhes font appel, expliitement ou
impliitement, àlanotionde ontinuation.Nousessayonsde préiserleurs relationset deomparer
leursperformanesautraversdemisesen÷uvre simplesdansleadredulangageOCaml.
2. Un modèle simple de onurrene
Nousproposonsunmodèlesimpledeonurrene,déniparlesprimitivessuivantes:
spawn prend un thunk et réeun nouveau thread àexéuter.Celui-i ne ommene sonexéution
quesi/quandlaprimitivestart aétéinvoquée.
yield suspendlethread,permettantauxautresdes'exéuter.
halt terminelethread.Sitouslesthreads sontterminés,start retourne.
start démarrelesthreadsréésparspawn et bloquejusqu'àe qu'ilssoienttousterminés.
stop stoppedenitivementtouslesthreads.start retourne.
Lesthreads peuventommuniquer àl'aide deMVars.Introduites enHaskell[14℄,les MVars sont
desvariablesmutablespartagéesquipermettentlaommuniationetlasynhronisationentrethreads.
Ondénitlestroisopérationssuivantes:
make mvar réeunenouvelleMVarvide.
take mvar retirelavaleurd'uneMVar.
put mvar plaeunevaleurdansune MVar.
Une MVar peutontenirune valeurouêtre vide.Lesopérationstake et put bloquentsilaMVar
est respetivement vide ou pleine. Pour simplier nous onsidérons que pour haqueMVar un seul
threadsouhaiteérireet unseulthreadsouhaitelireàunmomentdonné.
3. Deux appliations
Nousdérivonsi-dessousdeuxexemplesd'appliationsquinouspermettrontd'évaluerlestylede
programmationrequisparnotremodèleethaunedesesmisesen÷uvre.Ellesservirontaussiàfaire
quelquesomparaisonsdeperformanes.
3.1. Crible d'Ératosthène
Notrepremièreappliation estleribled'Ératosthène.Cetteversiononurrente estunlassique
notamment présentée dans [7℄ (qui indique que sa première mention est dans [13℄). Une variante
apparaît également dans [6℄. Le programme est struturé en une haîne de threads éhangeantdes
messages:
integers estlegénérateur,il émettouslesentiersàpartirde2,
lter n relaielesnombresqu'ilreçoits'ilsnesontpasmultiplesdesonparamètren,
sift réeetinsèreunnouveaultredanslahaîneàhaquenombrereçu,
output ahelesnombresqu'ilreçoit.
integers
mi sift
mo output
integers
mi filter 2
m sift
mo output
integers
mi filter 2
m filter 3
m’ sift
mo output
thread
mvar
spawn create
spawn cr.
Figure 1Leribleonurrent
Ainsileribleseonstruitommeunehaînedethreadsfilterenadrésparungénérateur(integers)
surlagauheetunétendeur(sift)suivid'unonsommateur(output)surladroite.Lesmessagessont
éhangésau moyen deMVars.Lagure 1montre lesthreads audémarrageet après que lepremier
puisledeuxièmenombrespremiersaientététrouvés.
Leode,visiblegure12enstylediret etindiret,estpartiulièrementsimple.
3.2. Tri onurrent
Notre seond exempleest un trionurrentdérit dans [6℄, qui explique quele tribulle et letri
parinsertionsontdesversionsséquentialisées deetalgorithme.
L'algorithmeutiliseun réseaudethreads comparateur très simples,haunutilisé pourtrier une
paire de valeurs depuis deux MVars d'entrée vers deux MVars de sortie. Un omparateur ave les
entréesx et y et lessorties hi and lo est montrégure 2(a). Lagure2(b) montre unréseau pour
trierunelistede4valeurs.
PSfragreplaements
x y
hi lo
(a)Comparateur
PSfragreplaements
x0
x1
x2
x3
r0 r1 r2 r3
(b)Réseaudetri
Figure2Trionurrent
LesMVarsn'apparaissentpassurlagure,ellesstokerontlesvaleursinitialesetnalesetserviront
pourlaommuniationentrelesomparateurs.Nousnemontronspasleodeparmanquedeplae.
4. Réalisations
Notre ordonnaneur sera une simple boule extrayantles threads d'une le FIFO (runq) et les
exéutant. Nous pouvonsd'ores et déjà implémenter les primitives start et stop. L'implémentation desautresprimitivesdépendradehaqueréalisation.
letrunq = Queue.create ()
letenqueue t = Queue.push t runq
letdequeue () = Queue.take runq
exeptionStop
letstop () = raise Stop
letstart () =
try
whiletruedo
dequeue () ()
done
withQueue.Empty | Stop → ()
4.1. Réalisations en style diret
Dansleasde laprogrammationenstyle diret, lesprimitivesaurontlessignaturesmontréessur
lagure3.Certainesdeesopérationssontpotentiellementbloquantesmaisseprésententommedes
fontionsordinaires.Leodeduribleest montrégure12(a).
Lesupportstandardd'OCamlpourlesthreads(systèmeouVM)peutêtreutilisépourréaliserles
primitivesaveessignatures.Nousnedérironspasetteréalisationquinousservirauniquementde
référenepouromparerlesperformanesdesréalisationslégères.
valyield : unit → unit
valspawn : (unit → unit) → unit valhalt :unit → unit
valstart :unit → unit valstop :unit → unit
typeαmvar
valmake mvar :unit → αmvar
valtake mvar : αmvar → α
valput mvar : αmvar → α → unit Figure3Signaturesdesprimitivespourlestylediret
Pour suspendre un thread et le réativer plus tard, nous devons pouvoir sauver la situation
ourante du thread. Cela orrespond à la notion de ontinuation, qui représente à un point de
l'exéutiond'unefontione quiresteàexéuter[2℄, 'estàdireleontextedel'éxéution.
Laprimitiveallwithurrentontinuation (oucall/cc)aétéintroduiteparlelangageSheme[8℄.
Celle-i réaliseune opie de (ouapture) laontinuationatuelle et laréie (la rend manipulable
parle programme)en une valeurde typeα cont.1 Les ontinuationspeuventainsi être manipulées expliitement par le programme omme n'importe quelle autre valeur. Une telle ontinuation de
première lasse peut être lanée (throw) ave un paramètre de type α auquel as elle érase la
ontinuation ourante, de façon que l'éxéution reprenne au point où la ontinuation avait été
apturée.Ceipermetdemanipulerleotdeontrleduprogrammeet enpartiulierd'introduirela
notiondethreads.
Nous avons réaliséune miseen ÷uvre baséesur call/cc similaireàelle dérite dans[4℄mais ne
laprésenteronspas ii: laseule miseen÷uvre existante decall/cc pourOCamlest dériteparson
auteurommetrèsnaïveavedesperformanesterribles [11℄.
Nous utiliseronslabibliothèqueaml-shift[15℄ qui oredesontinuations délimitées. Une telle
ontinuationestunpréxedeequiresteàexéuter,représentéeparuneportiondélimitéeduontexte.
Unetelleontinuation(aussiappeléepartielle,omposable,orsousontinuation)retourneunevaleur
etpeutdonêtreréutilisée etomposée.
La littérature dénit un ertain nombre d'opérateurs assoiés. L'idée générale est qu'une telle
ontinuation est onstruite en ommençant par pousser un délimiteur (ou prompt) sur la pile puis
en apturant la ontinuation jusqu'à un prompt. Dans ette bibliothèque push prompt pousse un
délimiteur, tandis que take subcont retire de la pile le fragmentjusqu'au prompt inlu et retourne
le fragment (sans le prompt) sous forme d'une valeurde type (α, β) subcont2 où α est le type de
la valeuràpasser aulaner de la ontinuation,β étantelui retourné par laontinuation. Enn,
push subcont pousse uneontinuationsurlapile(ie lalane).
Ainsi, pour suspendre un thread nous devons apturer sa ontinuation. Pour le réativer nous
devons pousser le prompt et la ontinuation. Nous hoisissons d'enapsuler immédiatement la
ontinuation apturée dans une fontion qui poussera le prompt et la ontinuation, e qui est le
omportementdel'opérateurshift0 (elui-iretirelepromptdelapileetl'inlutdanslaontinuation apturée)[9℄.Ainsil'ordonnaneurdoitsimplementexéuterlafontionpourrelanerlethread(voir
ledequeue () () danslaboulestart plushaut).
letprompt = new prompt ()
letshift0 p f = take subcont p (funsk () →
(f (func → push prompt subcont p sk (fun() → c))))
1. En Sheme, les ontinuations sont réiées sous forme de fontions. Appliquer la fontion revient à laner la
ontinuation.Ladesription quenous donnonsiiorrespond plusàe quipeut êtremisen÷uvre dansunlangage
typéstatiquementommeOCaml.
2. Ceomportementorrespondàl'opérateurcontrol0 [9℄.
letyield () = shift0 prompt (funf → enqueue f)
lethalt () = shift0 prompt (funf → ())
halt nettoie la pile ensupprimant leprompt et un éventuelreste de ontexte.La dénition de shift0 donnéei-dessusestuneversionoptimisée3 de
letshift0 p f = take subcont p (funsk () → (f (func → push prompt p
(fun() → push subcont sk (fun() → c)))))
spawnenapsulesonargumentdansunefontionquiommeneparpousserlepromptetsetermine
enappelanthalt defaçonàassurerquelepromptsoitretirédelapilequandlethreadtermine.
letspawn t = enqueue (fun() → push prompt prompt (fun() → t(); halt ()))
UneMVarestunsimplestrutavetroisvaleursdetypeoption:lavaleurstokéedanslaMVar,
le thread bloqué sur une opération take mvar, le thread bloqué sur une operation put mvar ave
lavaleurqu'il veut plaer. Quandun thread bloque, sa ontinuationest apturéeet enapsuléepar
shift0 puisstokéedanslehampapproprié.Lafontionserareplaéedanslaled'exéution quand
lethread seraréativé.Lesfailwith orrespondentànotrehypothèsesimpliatried'unseul thread souhaitantlireouériredansuneMVardonnéeàunmomentdonné.
typeαt = α → unit
typeαmvar = {mutablev:αoption;
mutableread: αt option;
mutablewrite: (unit t × α)option}
letmake mvar () = {v=None; read=None; write=None }
letput mvar out v =
mathout with
| {v=Some v′; read = ; write=None } →
shift0 prompt (funf → out.write ← Some (f,v))
| {v=None; read=Some r; write=None } →
out.read ← None; enqueue(fun() → r v)
| {v=None; read=None; write=None } → out.v ← Some v
| {v= ; read= ; write=Some } → failwith "put mvar"
lettake mvar inp =
mathinp with
| {v=Some v; read=None; write=None } → inp.v ← None; v
| {v=Some v; read=None; write=Some(c,v′)} → inp.v ← Some v′; inp.write ← None; enqueue c; v
| {v=None; read=None; write= } → shift0 prompt (funf → inp.read ← Some f)
| {v= ; read=Some ; write= } → failwith "take mvar"
4.2. Réalisations en style indiret
Dans le style indiret le ode des appliations est rédigé de manière à rendre les ontinuations
expliites (sous forme defermeture)à haquepoint deoopération. Ainsiles ontinuationspeuvent
êtremanipuléessansavoirreoursàuneprimitivedeapture.
3. Enfaitlaversionnonoptimiséeprésenteunefuitedemémoire(voir[10,AppendixB℄).
Voyez le ode du thread sift du rible sur la gure 12(b). Nous notons >>= (appelé bind)
l'opérateur deomposition séquentielle.
4
Ce opérateurest présentaux pointsde oopération,entre
uneopérationpotentiellementbloquanteetsaontinuation.Cetteontinuationestunefermeturequi
sera exéutée quand l'opération bloquante sera terminée, elle en reevra le résultat en paramètre.
Ce style impose de onvertir les boules impérativesen fontionsréursivessi ellesontiennent une
opérationbloquante.Nousdénironsuneopérationsupplémentaireenstyleindiret:skip,l'opération vide.
Nousdérivonsdans lasuitelaréalisationde notremodèleave lesstylestrampoline, monadique
(basé sur des ontinuations ou des promesses) et parévénements. Tous sont des variantes du style
indiret.
4.3. Style trampoline
Danslestyletrampoline[5℄haquefontionbloquante reçoitsa ontinuationenparamètre.C'est
une variante du style passage de ontinuation (CPS, [17℄) où les ontinuations ne sont rendues
expliitesqu'auxpointsdeoopération.
La gure 4 montre les signatures des opérations. Chaque opération potentiellement bloquante
prendenparamètresaontinuation,une fontionàexéuterquandl'opérationaétéréalisée.
valskip : (unit → unit) → unit
val(>>= ) : ((α → unit) → unit) → (α → unit) → unit valyield : (unit → unit) → unit
valspawn : (unit → unit) → unit valhalt :unit → unit
valstart :unit → unit valstop :unit → unit
typeαmvar
valmake mvar :unit → αmvar
valtake mvar : αmvar → (α → unit) → unit
valput mvar : αmvar → α → (unit → unit) → unit Figure4Signaturesdesprimitivespourlestyletrampoline
Par exemple l'opération yield (i-dessous àgauhe) prend en argument sa ontinuation, 'est à direlafontionàexéuterquand lethread seraréativé.Onpeutobtenirunesyntaxeplusplaisante
(i-dessousàdroite)sil'ondénit l'opérateurinxe >>= ommeappliquantsonpremierargument
au deuxième (la ontinuation) 5
et si nous adoptons une indentation reétant l'idée d'exéution en
séquene.
print string "hoho";
yield (fun() → print string "haha";
yield (fun() →
...
))
let(>>=)inst (k :α → unit) :unit =inst k
...
print string "hoho";
yield >>=fun() → print string "haha";
yield >>=fun() →
...
Rendrelesontinuationsexpliitesn'estpasnéessairementaussiintrusifqu'onpourraitlepenser.
Dansleodeduribleellesn'apparaissentenfaitjamais(arlesfontionssontréursivesinnies).
Ellesnedoiventêtrementionnéesquequandononstruitdesopérationsbloquantesomplexes,omme
4. C'estunempruntàlasyntaxedesmonadesmaisnereprésentepasnéessairementl'opérateurbinddesmonades
ommenousleverrons.
5. LesprogrammeursHaskellpenserontàl'opérateur$.
les deux exemples i-dessous. Le premier abstrait l'opération de suspension trois fois d'alée, le
deuxièmeletransfertd'unevaleurd'uneMVar àuneautre:
letyield3 k =
yield >>=fun() → yield >>=fun() → yield >>=
k
lettransfer mvar m1 m2 k = take mvar m1 >>=funv → put mvar m2 v >>=
k
Réalisation Une fontion bloquante, omme yield ou take mvar, reçoit sa ontinuation en paramètre,ellepeutl'exéuterimmédiatementou,sielledoitbloquer,lastokeravantderetourner.
Leodeesttrèsprohedeeluiutiliséavelesapturesdeontinuationsdélimitées:ladiérene
estquenousn'avonspasàlesapturerpuisqu'ellessontfourniesexpliitement.
letskip k = k ()
letyield k = enqueue k
lethalt () = ()
letspawn t = enqueue t
letclose k =fun() → k (fun → ())
lettake mvar inp k =
mathinp with
| {v=Some v; read=None; write=None } → inp.v ← None; k v
| {v=Some v; read=None; write=Some(c,v′)} → inp.v ← Some v′; inp.write ← None; enqueue c; k v
| {v=None; read=None; write= } → inp.read ← Some(k)
Unmotsurspawn:omposerdesfragmentsave>>=produitgénéralementunefontionouverte, prenantune ontinuation,qu'ilfautluifournirandelafermer avantdel'exéuterommethread.
Cependant les threads dénis pour le rible sont des réursions innies qui ne prennent pas untel
paramètre.Nousséparons donlafermeturede lafontiondulanementduthread (fontions close
etspawn).
4.4. Monade de ontinuation
Lamonadedeontinuationestdénie dans[3℄.Lesmonadessontutilespourgérerleseetsdans
unenvironnementfontionnelpur[14℄.Unemonadeestuntypeαt quiore(aumoins)lesprimitives return et bind (notéommeopérateurinxe>>=)montréesgure5.Commelesuggèrentlestypes, return v onstruitune valeurmonadiquede typeαt ontenant v etm >>= f ouvre m pour
extrairelavaleur v,ladonneàf etretournelavaleurmonadiqueretournéeparf.
Iinouspouvonsdénirtypeαt = (α → unit) → unit,une valeurdetypeαtestunefontion
prenantuneontinuation detypeα → unit.Nouspouvonsalorsvoirlesfontions bloquantesomme retournantune valeurdetypeαt (gure 5).
L'opérateur bind est un peu plus ompliqué que elui utilisé dans le style trampoline. Il prend enhargelagestiondesontinuationsqui n'ontplusàapparaîtreexpliitementdans laomposition
de fragments(nous avonshangé très légèrement la dénition de yield an de lui faireprendre un
argument()):
letyield3 () =
yield () >>=fun() → yield () >>=fun() → yield ()
lettransfer mvar m1 m2 = take mvar m1 >>=funv → put mvar m2 v
Pourrendreeipluslair,voiilesdénitionsdereturnetbind ainsiqueleurstypesdéveloppés:
typeαt
valreturn : α → αt
val(>>= ) : αt → (α → β t) → β t
valspawn : (unit → unit t) → unit valskip :unit t
valyield : unit → unit t
valhalt :unit → unit t
valstop :unit → unit t
valstart :unit → unit
typeαmvar
valmake mvar :unit → αmvar
valput mvar : αmvar → α → unit t
valtake mvar : αmvar → αt
Figure5Signaturesdesprimitivespourlestylemonadique
valreturn : α → (α → unit) → unit
letreturn a =funk → k a
val(>>=) : ((α → unit) → unit) → (α → (β → unit) → unit) → (β → unit) → unit
let(>>=)f k′ =funk → f (funr → k′ r k)
'estàdirequebind retourneunefontionquiaepteuneontinuation.Voyonslaformedéveloppée denotreexempletransfer mvar :
transfer mvar m1 m2 , funk → take mvar m1 (funr → (funv → put mvar m2 v)r k)
Enn,spawn fournitlaontinuationvideetplaelafontionenled'exéution.
letspawn (t :unit → unit t) = enqueue(fun() → t () (fun() → ()))
Comme on le voit, tout ei est très prohe du style trampoline. Celui-i a été proposé dans le
adredulangageSheme,onpeutvoirlamonadedeontinuationessentiellementommeuneversion
typéedustyletrampoline.
4.5. Monade de promesse
Une promesse [12℄ est une valeurqui peut être utilisée pour aéder plus tardà une valeur qui
n'estpasnéessairementdisponibleimmédiatement.
Une opération qui bloquerait peut retourner immédiatement une promesse.Une promesse peut
être prête si la valeur est disponible ou bloquée si elle ne l'est pas enore. La promesse peut être
simplementpasséejusqu'aupointoùlavaleurqu'ellereprésenteesteetivementrequise:l'opération
claim permetd'extraireette valeur(ellepeutbienévidemmentbloquer).
Lessignaturesdesopérationsserontlesmêmequepourlamonadedeontinuation(gure5).Les
opérations bloquantes retournentune promesse, leclaim étanteetué parbind. Dans t >>= f,
bind devrasoit:
passeràf lavaleurdet sielleest prête(etretourneralorslapromesseretournéeparf),ou
retournerunepromessebloquéeetfaireensorteque:
f reçoivelavaleurpromisedèsquet devientprête,
lapromessebloquéeretournéesoitlamême queelleretournéeparf.
Réalisation Voii une brève desription d'une réalisation possible (gure 6). Celle-i est très
largement inspirée de Lwt (light weigth threads) [18℄, une bibliothèque OCaml de threads légères.
Nousessayonsden'enreteniriiquelesaspetslesplusfondamentauxpourfailiter laomparaison
avelesautresréalisations.Nousinterprétonsii letypeαt ommeletypedes promessesalorsque
[18℄l'interprèteommeletypedesthreads.