Haskell Language
Konkurencja
Szukaj…
Uwagi
Dobrym źródłem wiedzy na temat programowania współbieżnego i równoległego w Haskell są:
Spawning Threads with `forkIO`
Haskell obsługuje wiele form współbieżności, a najbardziej oczywistym jest rozwidlanie wątku za pomocą forkIO
.
Funkcja forkIO :: IO () -> IO ThreadId
wykonuje akcję IO
i zwraca jej ThreadId
, tymczasem akcja zostanie uruchomiona w tle.
Możemy to zwięźle przedstawić za pomocą ghci
:
Prelude Control.Concurrent> forkIO $ (print . sum) [1..100000000]
ThreadId 290
Prelude Control.Concurrent> forkIO $ print "hi!"
"hi!"
-- some time later....
Prelude Control.Concurrent> 50000005000000
Obie akcje będą działać w tle, a druga prawie na pewno zakończy się przed ostatnią!
Komunikacja między wątkami za pomocą `MVar`
Bardzo łatwo jest przekazywać informacje między wątkami za pomocą MVar a
type i towarzyszących mu funkcji w Control.Concurrent
:
-
newEmptyMVar :: IO (MVar a)
- tworzy nowyMVar a
-
newMVar :: a -> IO (MVar a)
- tworzy nowyMVar
o podanej wartości -
takeMVar :: MVar a -> IO a
- pobiera wartość z podanegoMVar
lub bloków, dopóki nie będzie dostępna -
putMVar :: MVar a -> a -> IO ()
- umieszcza podaną wartość wMVar
lub blokach, aż będzie pusta
Zsumujmy liczby od 1 do 100 milionów w wątku i poczekaj na wynik:
import Control.Concurrent
main = do
m <- newEmptyMVar
forkIO $ putMVar m $ sum [1..10000000]
print =<< takeMVar m -- takeMVar will block 'til m is non-empty!
Bardziej złożoną demonstracją może być pobranie danych przez użytkownika i podsumowanie w tle podczas oczekiwania na więcej danych:
main2 = loop
where
loop = do
m <- newEmptyMVar
n <- getLine
putStrLn "Calculating. Please wait"
-- In another thread, parse the user input and sum
forkIO $ putMVar m $ sum [1..(read n :: Int)]
-- In another thread, wait 'til the sum's complete then print it
forkIO $ print =<< takeMVar m
loop
Jak wspomniano wcześniej, jeśli wywołasz takeMVar
a MVar
będzie pusty, blokuje się, dopóki inny wątek nie umieści czegoś w MVar
, co może spowodować problem z filozofią MVar
. To samo dzieje się z putMVar
: jeśli jest pełne, będzie blokować, aż będzie puste!
Weź następującą funkcję:
concurrent ma mb = do
a <- takeMVar ma
b <- takeMVar mb
putMVar ma a
putMVar mb b
MVar
dwie funkcje z niektórymi MVar
concurrent ma mb -- new thread 1
concurrent mb ma -- new thread 2
Co może się zdarzyć, to:
- Wątek 1 czyta
ma
i blokujema
- Wątek 2 odczytuje
mb
a tym samym blokujemb
Teraz wątek 1 nie może odczytać mb
ponieważ wątek 2 go zablokował, a wątek 2 nie może odczytać ma
ponieważ wątek 1 go zablokował. Klasyczny impas!
Bloki atomowe z programową pamięcią transakcyjną
Innym potężnym i dojrzałym narzędziem do współbieżności w Haskell jest Software Transactional Memory, która pozwala na zapis wielu wątków do jednej zmiennej typu TVar a
w sposób atomowy.
TVar a
jest głównym typem związanym z TVar a
STM
i oznacza zmienną transakcyjną. Są one używane podobnie jak MVar
ale w MVar
STM
poprzez następujące funkcje:
atomically :: STM a -> IO a
Wykonaj serię akcji STM atomowo.
readTVar :: TVar a -> STM a
Przeczytaj wartość TVar
, np .:
value <- readTVar t
writeTVar :: TVar a -> a -> STM ()
Napisz wartość do podanego TVar
.
t <- newTVar Nothing writeTVar t (Just "Hello")
Ten przykład pochodzi z Wiki Haskell:
import Control.Monad import Control.Concurrent import Control.Concurrent.STM main = do -- Initialise a new TVar shared <- atomically $ newTVar 0 -- Read the value before <- atomRead shared putStrLn $ "Before: " ++ show before forkIO $ 25 `timesDo` (dispVar shared >> milliSleep 20) forkIO $ 10 `timesDo` (appV ((+) 2) shared >> milliSleep 50) forkIO $ 20 `timesDo` (appV pred shared >> milliSleep 25) milliSleep 800 after <- atomRead shared putStrLn $ "After: " ++ show after where timesDo = replicateM_ milliSleep = threadDelay . (*) 1000 atomRead = atomically . readTVar dispVar x = atomRead x >>= print appV fn x = atomically $ readTVar x >>= writeTVar x . fn