Kann eine Transaktion zwei verschiedene TVar
s auf atomare Weise aktualisieren? d. h. kann ich Datenstrukturen aus vielen TVar
s zusammensetzen, um Konflikte zu reduzieren? Wenn ja, könnten Sie ein Beispiel geben?Haskell: Zwei oder mehr TVars automatisch aktualisieren. Möglich?
Antwort
Kann eine Transaktion zwei verschiedene TVars auf atomare Weise aktualisieren?
Ja, Sie können mehrere TVars automatisch in einer Transaktion aktualisieren. Das ist der Sinn von STM. Es wäre nicht sehr nützlich, wenn Sie nicht könnten.
Kann ich Datenstrukturen aus vielen TVars zusammensetzen, um Konflikte zu reduzieren? Wenn ja, könnten Sie ein Beispiel geben?
Hier ist ein (etwas albernes) Beispiel zum Speichern von TVars in einer Datenstruktur. Es simuliert eine Reihe zufälliger gleichzeitiger Transaktionen zwischen Konten in einer Bank, wobei jedes Konto nur eine TVar Integer
ist. Die Konto-TV-Programme werden in einer Karte aus Konto-IDs gespeichert, die ihrerseits in einer TVar-Datei gespeichert werden, sodass neue Konten im laufenden Betrieb erstellt werden können.
import Control.Concurrent
import Control.Concurrent.MVar
import Control.Concurrent.STM
import Control.Monad
import System.Random
import qualified Data.Map as Map
type AccountId = Int
type Account = TVar Dollars
type Dollars = Integer
type Bank = TVar (Map.Map AccountId Account)
numberOfAccounts = 20
threads = 100
transactionsPerThread = 100
maxAmount = 1000
-- Get account by ID, create new empty account if it didn't exist
getAccount :: Bank -> AccountId -> STM Account
getAccount bank accountId = do
accounts <- readTVar bank
case Map.lookup accountId accounts of
Just account -> return account
Nothing -> do
account <- newTVar 0
writeTVar bank $ Map.insert accountId account accounts
return account
-- Transfer amount between two accounts (accounts can go negative)
transfer :: Dollars -> Account -> Account -> STM()
transfer amount from to = when (from /= to) $ do
balanceFrom <- readTVar from
balanceTo <- readTVar to
writeTVar from $! balanceFrom - amount
writeTVar to $! balanceTo + amount
randomTransaction :: Bank -> IO()
randomTransaction bank = do
-- Make a random transaction
fromId <- randomRIO (1, numberOfAccounts)
toId <- randomRIO (1, numberOfAccounts)
amount <- randomRIO (1, maxAmount)
-- Perform it atomically
atomically $ do
from <- getAccount bank fromId
to <- getAccount bank toId
transfer amount from to
main = do
bank <- newTVarIO Map.empty
-- Start some worker threads to each do a number of random transactions
workers <- replicateM threads $ do
done <- newEmptyMVar
forkIO $ do
replicateM_ transactionsPerThread $ randomTransaction bank
putMVar done()
return done
-- Wait for worker threads to finish
mapM_ takeMVar workers
-- Print list of accounts and total bank balance (which should be zero)
summary <- atomically $ do
accounts <- readTVar bank
forM (Map.assocs accounts) $ \(accountId, account) -> do
balance <- readTVar account
return (accountId, balance)
mapM_ print summary
putStrLn "----------------"
putStrLn $ "TOTAL BALANCE: " ++ show (sum $ map snd summary)
Dies sollte eine Gesamtbilanz von Null am Ende drucken, wenn es während der Transfers keine Rennbedingungen sind.
Eine Transaktion ist vollständig atomar; wenn es mehrere TVar
s ändert, werden beide Änderungen gemeinsam, atomar, isoliert geschehen. Alles, was in einem einzigen atomically
Block ausgeführt wird, ist eine einzelne Transaktion. Zum Beispiel:
swap :: (Num a) => TVar a -> TVar a -> STM()
swap v1 v2 = do
a <- readTVar v1
b <- readTVar v2
writeTVar v1 b
writeTVar v2 a
Hier swap a b
wird atomar zwei TVar
s tauschen. Die Zusammensetzbarkeit von atomaren Transaktionen auf diese Weise ist einer der wichtigsten Vorteile von STM.
Schöne Antwort. Ich habe eine Frage. Kann der atomare Block von "randomTransaction" nach "transfer" verschoben werden? Was ich meine, ist "atomar" von "randomTransaction" zu entfernen und den ganzen Körper von "transfer" mit "atomically" zu umhüllen. –