Ett av de riktigt svåra problem med fler-trådad (multi-threaded) programmering är att hantera uppdateringar av gemensamma data, eller mer precist uttryckt Shared Mutual State. Det klassiska sättet är att omge operationerna med mutex-lås (MUTual EXclusion), så att högst en tråd kan accessa data i taget. Detta är i regel ett nödvändigt men icke tillräckligt krav för att hantera mer komplexa interaktioner. Dessutom behövs villkorsvariabler (condition variables), som modellerar ett önskat sub-system tillstånd. T.ex. "vänta här brevlådan är tom". Det finns tre villkor för att problem med gemensamt data ska uppstå:
- En icke-atomär sekvens av READ, MODIFY, WRITE på ett data-element.
- Mer än 1 tråd som accessar datat.
- Tråd-växling inuti kod-sekvensen.
Samtliga måste vara uppfyllda. Strategin är att bryta mot minst ett av villkoren. T.ex. konstant-data (immutable) bryter mot villkor 1 och mutex-lås bryter mot villkor 2.
Erlang gör rent hus med detta, genom att det inte är möjligt att skapa globala data. I stället kapslar man in data i en Erlang tråd (aka Erlang process) och sen får nyttjare skicka asynkrona meddelanden till tråden i fråga, som sedan behandlar meddelanden ett-i-taget. Det här är kärnan i den s.k. Actors-modellen, som på senare tid har blivit kolossalt populär i språk som Groovy-GPars och Scala-Akka.
En Erlang process, är ett slags mellanting mellan objekt och tråd. Objekt såtillvida att dess tillstånd är inkapslat och tråd såtillvida att den exekverar samtidigt med andra. Ett Erlang program kan sägas bestå av en soppa av Erlang processer som simmar omkring och kastar asynkrona meddelanden på varandra. Mellan processerna, finns ingenting.
Utmärkande för Erlang är också single-assignment property. Detta innebär att en variabel är antingen obunden eller bunden (för tid-och-evighet) till ett visst värde. M.a.o. bryter vi mot villkor 1ovan. En konsekvens är att man inte kan ha traditionella (for-) loopar, med en loop-variabel som stegas igenom. I stället används genomgående rekursion som upprepnings-mekanism i språket. Här förutsätter det att kompilatorn gör s.k. svans-rekursions-optimering (tail-recursion optimization).
Nå, låt oss kika på lite programkod. Så här kan vi representera ett enkelt bank-konto, som en Erlang tråd.
account_new() -> spawn(?MODULE, account_body, [0]). account_body(Balance) -> receive {balance, Amount, Sender} -> BalanceNew = Balance + Amount, Sender ! BalanceNew, account_body(BalanceNew); stop -> ok end.Function account_new() -kontruktor- skapar en ny tråd mha den inbyggda funktionen spawn(). Själva tråd-kroppen loopar via rekursion och innehåller trådens inkapslade tillstånd (state) som parameter. Tråden ligger och väntar på ett inkommande meddelande och uppdaterar sedan saldot, samt skickar detta tillbaka till sändare. Här kan man se Erlangs typiska send-operator '!', hämtad från CSP. För att läsa och ändra saldo har jag lagt till följande funktion, som skickar ett meddelande och sen väntar på ett svar (extended rendezvous).
account_balance(Account, Amount) -> Account ! {balance, Amount, self()}, receive BalanceNew -> BalanceNew end.En uppdateringstråd loppar ett visst antal gånger och sätter in 100, samt loopar lika många gånger och tar ut 100. Netto-resultat är alltså 0, vilket är förväntat resultat av programmet. Så här ser uppdateraren ut.
updater_new(Id, Account, NumTransactions, Bank) -> spawn(?MODULE, updater_body, [Id, Account, NumTransactions, Bank]). updater_body(Id, Account, NumTransactions, Bank) -> repeat(NumTransactions, fun() -> account_balance(Account, +100) end), repeat(NumTransactions, fun() -> account_balance(Account, -100) end), Bank ! Id. repeat(N, _) when N =< 0 -> ok; repeat(N, Stmt) when N > 0 -> Stmt(), repeat(N-1, Stmt).Konstruktorn skapar en ny tråd och skickar med info om id till kontot och antal transaktioner att utföra. Tråd-kroppen loopar mha funktion repeat() som tar ett heltal plus en closure, i vilken kontot uppdateras.
Huvudprogrammet skapar först kontot och sen önskat antal uppdaterare. Sen ligger det och väntar på att dessa är klar, samt hämtar slutsaldot och avslutar kontot.
start() -> start(10, 1000). start(NumUpdaters, NumTransactions) -> Account = account_new(), [updater_new(Id, Account, NumTransactions, self()) || Id <- lists:seq(1,NumUpdaters)], repeat(NumUpdaters, fun() -> receive Id when is_integer(Id) -> ok end end), FinalBalance = account_balance(Account, 0), Account ! stop, {final_balance, FinalBalance}.Uppdaterarna skapas med en liten finess som kallas list-comprehension och kan väl lite (mycket) slarvigt sägas vara en for-loop. Till höger om '||' finns en lista med talen [1,2,..,N] och en lopp-variabel (Id). Denna används till vänster om avdelaren i ett list-element-konstruktions-uttryck (oj, långt sammansatt ord), som skapar en ny uppdaterar-tråd. Sen väntar vi på att alla uppdaterare har terminerat. Efter det hämtas saldot och vi är klara. Så här kan en körning se ut.
Här kan du se att jag först kompilerar programmet (bank.erl) och sen kör det med 100 uppdaterare och 100.000 transaktioner. Programkoden i sin helhet har jag lagt upp här:
Verkar Erlang intressant? Kolla då in kursen:
Skicka en kommentar
Trevligt att du vill dela med dig av dina åsikter! Tänk på att hålla på "Netiketten" och använda vårdat språk.