Samtidigt Haskell
Concurrent Haskell utökar Haskell 98 med explicit samtidighet . Dess två huvudsakliga underliggande koncept är:
- En primitiv typ
MVar α
som implementerar en avgränsad/enplats asynkron kanal , som antingen är tom eller har ett värde av typenα
. - Möjligheten att skapa en samtidig tråd via
forkIO-
primitiven.
Uppbyggd ovanpå detta är en samling användbara samtidighets- och synkroniseringsabstraktioner såsom obegränsade kanaler , semaforer och exempelvariabler.
Haskell-trådar har mycket låg overhead: skapande, kontextväxling och schemaläggning är alla interna i Haskell-körtiden. Dessa trådar på Haskell-nivå mappas till ett konfigurerbart antal trådar på OS-nivå, vanligtvis en per processorkärna .
Programvarutransaktionsminne
Programvarutransaktionsminnet (STM)-tillägget till GHC återanvänder processen för att forfla primitiver från Concurrent Haskell . STM dock:
- undviker
MVar
s till förmån förTVar
s. - introducerar primitiverna
om försök
ochorElse
, vilket gör att alternativa atomära åtgärder kan sammanställas .
STM-monad
STM- monaden är en implementering av mjukvarutransaktionsminne i Haskell. Det är implementerat i GHC och gör det möjligt att ändra föränderliga variabler i transaktioner .
Traditionellt tillvägagångssätt
Betrakta en bankapplikation som ett exempel, och en transaktion i den - överföringsfunktionen, som tar pengar från ett konto och sätter dem på ett annat konto. I IO-monaden kan detta se ut så här:
typ Konto = IORef Heltalsöverföring :: Heltal -> Konto - > Konto -> IO () överföringsbelopp från till = gör fromVal <- readIORef from -- (A) toVal < - readIORef to writeIORef from ( fromVal - summa ) writeIORef to ( toVal + belopp )
Detta orsakar problem i samtidiga situationer där flera överföringar kan ske på samma konto samtidigt . Om det fanns två överföringar som överför pengar från konto från
, och båda samtalen att överföra körde linje (A)
innan någon av dem hade skrivit sina nya värden, är det möjligt att pengar skulle sättas in på de andra två kontona, med endast ett av kontona. belopp som överförs tas bort från kontot från
, vilket skapar ett tävlingsvillkor . Detta skulle lämna bankansökan i ett inkonsekvent tillstånd.
En traditionell lösning på ett sådant problem är låsning. Till exempel kan lås placeras runt modifieringar av ett konto för att säkerställa att krediter och debiteringar sker atomärt. I Haskell utförs låsning med MVars:
typ Konto = MVar Heltalskredit :: Heltal -> Konto - > IO () kreditbelopp konto = gör nuvarande <- taMVar konto putMVar konto ( aktuell + belopp ) debitering :: Heltal -> Konto - > IO ( ) debetbelopp konto = gör nuvarande <- taMVar konto putMVar konto ( aktuellt - belopp )
Att använda sådana procedurer kommer att säkerställa att pengar aldrig kommer att gå förlorade eller vinnas på grund av felaktig interfoliering av läsningar och skrivningar till ett individuellt konto. Men om man försöker komponera dem tillsammans för att skapa en procedur som överföring:
överför :: Heltal -> Konto - > Konto -> IO ( ) överför belopp från till = gör debiteringsbelopp från kreditbelopp till
ett tävlingsvillkor existerar fortfarande: det första kontot kan debiteras, sedan kan körningen av tråden avbrytas, vilket lämnar kontot som helhet i ett inkonsekvent tillstånd. Därför måste ytterligare lås läggas till för att säkerställa korrektheten av sammansatta operationer, och i värsta fall kan man behöva helt enkelt låsa alla konton oavsett hur många som används i en given operation.
Atomtransaktioner
För att undvika detta kan man använda STM-monaden, som gör att man kan skriva atomtransaktioner. Detta innebär att alla operationer inuti transaktionen är helt slutförda, utan att några andra trådar ändrar de variabler som vår transaktion använder, eller så misslyckas den, och tillståndet rullas tillbaka till där det var innan transaktionen påbörjades. Kort sagt, atomtransaktioner slutförs antingen helt eller så är det som om de aldrig genomfördes alls. Den låsbaserade koden ovan översätts på ett relativt enkelt sätt:
typ Konto = TVar Heltal kredit :: Heltal -> Konto -> STM () kreditbelopp konto = gör nuvarande <- readTVar konto skrivTVar konto ( aktuell + belopp ) debitering :: Heltal -> Konto - > STM ( ) debetbelopp konto = gör nuvarande <- readTVar konto skrivTVar konto ( aktuell - belopp ) överföring :: Heltal -> Konto - > Konto -> STM () överför belopp från till = gör debitera belopp från kreditbelopp till
Returtyperna för STM ()
kan användas för att indikera att vi komponerar skript för transaktioner. När det är dags att faktiskt utföra en sådan transaktion, används en funktion atomiskt :: STM a -> IO a .
Ovanstående implementering kommer att se till att inga andra transaktioner stör variablerna den använder (från och till) medan den körs, vilket gör att utvecklaren kan vara säker på att tävlingsförhållanden som ovan inte uppstår. Fler förbättringar kan göras för att se till att någon annan " affärslogik " bibehålls i systemet, dvs att transaktionen inte ska försöka ta pengar från ett konto förrän det finns tillräckligt med pengar i det:
0
överför :: Heltal -> Konto -> Konto - > STM ( ) överför belopp från till = gör frånVal <- läsTVar från if ( frånVal - belopp ) >= debitera sedan belopp från kreditbelopp för att annars försöka igen
Här har funktionen Försök igen
använts, som återställer en transaktion och försöker igen. Att försöka igen i STM är smart eftersom det inte kommer att försöka köra transaktionen igen förrän en av variablerna den refererar till under transaktionen har modifierats av någon annan transaktionskod. Detta gör STM-monaden ganska effektiv.
Ett exempelprogram som använder överföringsfunktionen kan se ut så här:
0
modul Main där import Control.Concurrent ( forkIO ) import Control.Concurrent.STM import Control.Monad ( för alltid ) import System.Exit ( exitSuccess ) typ Konto = TVar Heltal main = do bob <- newAccount 10000 jill <- newAccount 4000 repeatIO 2000 $ forkIO $ atomically $ överföra 1 bob jill för alltid $ do bobBalance <- atomically $ readTVar bob jillBalance <- atomically $ readTVar jill putStrLn ( "Bobs balans: " ++ visa bobBalance ++ ", Jills balans: " ++ visa jillBalance ) om bobBalance == 8000 sedan exitSuccess else putStrLn "Försöker igen." repeatIO :: Heltal -> IO a - > IO a repeatIO 1 m = m upprepaIO n m = m >> repeatIO ( n - 1 ) m nyttKonto :: Heltal - > IO Konto nyttKontobelopp = nyTVarIO beloppsöverföring :: Heltal -> Konto -> Konto -> STM ( ) överför belopp från till = gör frånVal < - läsTVar från if ( frånVal - belopp ) >= debitera sedan belopp från kreditbelopp för att annars försöka kreditera igen :: Heltal -> Konto -> STM () kreditbelopp konto = gör aktuellt <- readTVar konto skrivTVar konto ( aktuellt + belopp ) debet :: Heltal -> Konto - > STM () debetbelopp konto = gör nuvarande < - readTVar konto skrivTVar konto ( aktuellt - belopp )
som ska skriva ut "Bobs saldo: 8000, Jills saldo: 6000". Här har den atomära
funktionen använts för att köra STM-aktioner i IO-monaden.
- ^ Simon Peyton Jones, Andrew D. Gordon och Sigbjörn Finne. Samtidigt Haskell . ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages (PoPL). 1996. (Vissa avsnitt är inaktuella med avseende på den nuvarande implementeringen.)
- ^ Haskell Hierarchical Libraries , Control.Concurrent Arkiverad 2012-08-02 på archive.today
- ^ Tim Harris, Simon Marlow, Simon Peyton Jones och Maurice Herlihy . Komponerbara minnestransaktioner . ACM Symposium on Principles and Practice of Parallel Programming 2005 (PPoPP'05). 2005.
- ^ Control.Concurrent.STM