Вот чем мне нравится язык Scala , это тем, что, в отличие от ряда других языков, он не ограничивает выбор программиста. Если нужны мьютексы, используем мьютексы. Хотим атомарные переменные — пожалуйста. Нужны акторы — да вот же они . Агенты, параллельные коллекции — все к вашим услугам, хоть на volatile’ах многопоточность стройте. Ну и, конечно же, в Scala есть STM, которая, как и многие другие удачные решения, были перенесены в язык из мира Haskell.

Поскольку мы уже работали с STM в Haskell , не будем подробно останавливаться на том, что это такое и зачем нужно. Если вы не читали соответствующую заметку, рекомендую ознакомиться хотя бы с первыми тремя-четырьмя ее абзацами. Прочим же напоминаю главное, что нужно помнить об STM. STM дает нам атомарность изменений, вносимых в изменяемое состояние, согласованность данных в любой момент времени, а также изолированность от транзакций, выполняемых параллельно. Работая с STM очень трудно сделать дэдлок (но, как мы уже выясняли, в целом возможно). Если внутри транзакции будет брошено исключение, данные останутся в согласованном состоянии. Кроме того, функции, использующие STM, хорошо компонуются друг с другом.

Все это добро есть в библиотеке ScalaSTM . Пользоваться ей очень просто. Не будем тянуть кота за все его подробности, лучше сразу рассмотрим классический пример со счетами и деньгами на них:

package me. eax . stm_example

import scala. annotation . tailrec
import scala. concurrent . _
import scala. concurrent . duration . _
import scala. concurrent . stm . _
import scala. util . _
import scala. concurrent . ExecutionContext . Implicits . global

object AccountStates {
val accountsNum = 10
var states : Map [ String, Ref [ Int ] ] = {
for ( i < 0 until accountsNum )
yield s «account$i» > Ref ( 1000 )
} . toMap

def randomAccount : String = s «account${Random.nextInt(accountsNum)}»

def transfer ( fromAccount : String, toAccount : String, amount : Int )
( implicit txn : InTxn ) {
states ( fromAccount ) ( ) = states ( fromAccount ) ( ) — amount
states ( toAccount ) ( ) = states ( toAccount ) ( ) + amount
}

@ tailrec
def runTransactions ( threadNumber : Int, transactionNumber : Int ) {
if ( transactionNumber <= 0 ) return
val fromAccount = randomAccount
var toAccount = «»
do toAccount = randomAccount while ( fromAccount == toAccount )
atomic { implicit txn =>
val amount = Random. nextInt ( states ( fromAccount ) . get )
println (
s «[$threadNumber-$transactionNumber] Transferring $amount » +
s «USD from $fromAccount to $toAccount…» )
transfer ( fromAccount, toAccount, amount )
}
runTransactions ( threadNumber, transactionNumber — 1 )
}
}

object StmExample extends App {
val futuresSeq = {
for ( thrNum < 1 to 10 )
yield {
val f = Future { AccountStates. runTransactions ( thrNum, 100 ) }
f onFailure { case e => println ( s «Future $thrNum failed: $e» ) }
f
}
}

val fResults = Future. sequence ( futuresSeq )
Await. ready ( fResults, Duration. Inf )

println ( «FINAL STATE:» )
val finalState = {
atomic { implicit txn =>
for ( ( acc, balance ) < — AccountStates. states )
yield acc — > balance ( )
}
}

for ( ( acc, balance ) < — finalState ) {
println ( s «$acc -> $balance» )
}

println ( s «TOTAL: ${finalState.values.sum}» )
}

В начале мы имеем 10 счетов, на каждом из которых лежит по 1000$. Заметьте, что состоянием счета является не Int, а Ref[Int]. Ref — это такой специальный контейнер, работать с которым можно только внутри транзакции (аналог TVar в Haskell). Сама же транзакция создается с помощью функции atomic. Обратите внимание, как то, что в Haskell решается с помощью монад , в Scala достигается при помощи более понятных и легко компонуемых неявных аргументов. Внутри транзакции c Ref’ами можно работать при помощи x.get() и x.set(...) , либо при помощи более лаконичной записи x() и x() = ... , которая делает то же самое. В данном примере создается 10 футур , которые случайным образом бросают деньги со счета на счет таким образом, чтобы счета не уходили в минус. Специально для демонстрации компонуемости кусков кода, работающих с STM, была выделена функция transfer. Перед завершением работы программа выводит состояние всех счетов и суммарное количество денег на них:

FINAL STATE:
account2 -> 27
account6 -> 691
account9 -> 1773
account1 -> 422
account5 -> 1316
account0 -> 671
account8 -> 3481
account4 -> 1272
account7 -> 167
account3 -> 180
TOTAL: 10000

Как видите, денег в системе не прибавилось и не убавилось, что есть именно то, чего мы и ожидали. Обратите внимание, с какой легкостью мы оперируем счетами! Представьте, если бы мы пытались решить ту же самую задачу при помощи акторов, каких-нибудь агентов или мьютексов . Это был бы просто кошмар! Ну или все потоки упирались бы в одно и то же место.

В приведенном примере внутри транзакции использовались некоторые функции с побочными эффектами — nextInt и println. В этом нет ничего плохого, но в общем случае вы не должны так делать, так как код внутри блока atomic вообще может повторяться, а вы вряд ли хотите, чтобы ваша система многократно отправляла письма или типа того. Можете провести эксперимент, изменив код примерно таким образом:

var attempt = 0
atomic { implicit txn =>
attempt = attempt + 1
if ( attempt > 1 ) {
println ( s «[$threadNumber] Retried, attempt = $attempt» )
}
// …
}

Вы заметите, что большинство транзакций выполняются успешно с первой попытки, но в редких случаях транзакция начинает выполняться сначала, возможно, даже не один раз. Такое происходит в случае, если во время выполнения транзакции использованное состояние было изменено другими потоками. Также программист может сам перезапустить транзакцию с помощью функции retry:

atomic { implicit txn =>
// …
if ( someConditionWasViolated ( ) ) retry
// …
}

Следует однако отметить, что при вызове retry транзакция будет выполнена повторно только после изменении Ref’ов, прочитанных перед вызовом retry. Действительно, было бы странно повторять транзакцию вновь и вновь при условии, что ничего не изменилось. Если же перед вызовом retry никакие Ref’ы не читались, вы получите такое исключение:

java.lang.IllegalStateException: explicit retries cannot succeed
because cumulative read set is empty

За кадром остались транзакционные коллекции вроде TMap и TSet, комбинаторы типа orAtomic, и не только. Но я уверен, что теперь, ознакомившись с основами использования STM в Scala, вы без труда разберетесь в этих и других вопросах самостоятельно.

Ссылки по теме:

А используете ли вы ScalaSTM в своих проектах?

admin

Share
Published by
admin

Recent Posts

Что такое Zulip

Zulip — программное обеспечение для реализации корпоративного чата. Разработан в 2012 году, в 2014 был…

2 месяца ago

Что такое Zookeeper

Zookeeper — cервис-координатор, который позволяет обеспечить контроль синхронизации данных. Разработан на Java компанией Apache Software…

2 месяца ago

Что такое Zimbra

Zimbra — программное обеспечение для реализации почтового сервиса или, если сказать точнее, автоматизации совместной деятельности…

2 месяца ago

Что такое Zabbix

Zabbix — бесплатная система мониторинга. Позволяет отслеживать состояние сетевых узлов, компьютеров и серверов. Возможности: Поддержка…

2 месяца ago

Что такое YouTube

YouTube — компания-владелец одноименного портала для просмотра и хранения видео. Чтобы пользоваться данным порталом достаточно…

2 месяца ago

Что такое yota

Yota — провайдер, предоставляющий доступ к сети Интернет по беспроводной связи. Впервые, сервис начал работать…

2 месяца ago