Страничка курса: https://maxcom.github.io/scala-course-2022/
В JVM есть и то, и другое. В других средах может быть что-то одно.
В серверных проложениях часто выделяют по потоку каждому клиенту.
Программисту это удобно, но не всегда эффективно. В 7-й лекции поговорим об устройстве высоконагруженных приложений.
Синхронная функция:
def func(input: String): String
Иногда работа не выполняется в нашей функции:
Зачем?
Асинхронная функция:
def func(input: String, onComplete: Try[String] => Unit): Unit
Проблема: не удобно и не ФП. Callback hell.
Еще бывает Cancellable
val thread = new Thread(() => {
println("Hello world!")
})
thread.start()
Явно потоки (почти) никогда не нужно создавать:
Верхняя граница у современных ОС - порядка 1000 потоков на процесс. Дальше не эффективно.
// используем Java API
val executor: ExecutorService =
Executors.newFixedThreadPool(10) // 10 потоков
// создаем Scala-обертку
// часто используют "implicit val"
val ec: ExecutionContext =
ExecutionContext.fromExecutor(executor)
Задачи можно передать так:
ec.execute(() => {
println("Hello world!")
})
Thread Pool = пул потоков + очередь задач
Стандартные реализации:
def run(): Unit = for (i <- 0 to 10) println(i)
(0 to 5).foreach { _ =>
ec.execute(run _)
}
разный порядок при каждом запуске - сложно обеспечить корректность и протестировать
Например RandomAccessFile:
нельзя использовать из разных потоков
Большинство объектов и структур - не потокобезопасные.
Исключение: неизменяемые данные, но есть проблема их "публикации".
// наследие Java 1.0 - любой объект имеет lock
val lock = new Object()
// примитивные типы не имеют lock'ов
var counter: Int = 0
lock.synchronized {
// инкремент - не атомарная операция
counter += 1
}
Синхронизация на this - плохо из-за его публичности
val lock = new ReentrantLock()
var data = Vector(42)
def readData(): Vector[Int] = {
try { // всегда оборачиваем в try/finally
lock.tryLock(1, TimeUnit.MINUTES)
data
} finally {
lock.unlock()
}
}
Взаимная блокировка, из которой не выйти.
Пример: передача денег между счетами.
Dealock'ы бывают хитрые, но сводятся к той же схеме.
Пример: извлечение объектов парами из ограниченного пула.
Модель исполнения и памяти сложнее, чем кажется:
Модель памяти Java сложна - подробности смотрите Java Memory Model прагматика модели
// медленнее обычного var, но быстрее блокировки
@volatile
var vcounter: Int = 0
операции над ней "упорядочены", но защиты от "гонки потоков" нет
Специальная инструкция процессора - CompareAndSet (CAS)
Меняет значение на новое, если старое равно заданному
Потокобезопасно
// метод класса java.util.concurrent.AtomicInteger
// реализация из исходников JDK; Java
public final int incrementAndGet() {
for (;;) { // retry при конфликте
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
дешевле lock, если логика простая
Такой подход может использоваться для списков, деревьев и хеш-таблиц.
Разделяемое изменяемое состояние + параллелизм = проблемы.
Нет хорошего способа расставить блокировки в сложной системе.
Блокировки требуют "протокола" использования. Язык и runtime его не верифицируют.
Декомпозиция не работает - проблемы возникают в момент интеграции.
Качественно расставить блокировки не всегда удается:
Решение: разделяемое изменяемое состояние + параллелизм.
Делаем всю работу в одном потоке.
Пример: СУБД Redis, веб-сервер nginx, сервер приложений node.js
Решение: разделяемое изменяемое состояние + параллелизм.
Используем иммутабельные структуры и
цепочки обработки.
Пример: Future, Task, IO, Akka Streams
Решение: разделяемое изменяемое состояние + параллелизм.
Изменяемое состояние приватное, работаем с ним из одного потока.
Пример: акторы (Akka и др)
Вторая часть - Future и Promise.
(+ монадные трансформеры)
Future - результат отложенного вычисления.
от асинхронной функции
def func(input: String, onComplete: Try[String] => Unit): Unit
переходим к
implicit val ec: ExecutionContext = ???
val f = def func(input: String): Future[String]
f.onComplete(...) // добавляем Callback
это базовый низкоуровневый механизм
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
val f: Future[Int] = ???
implicit val ec: ExecutionContext = ???
f.onComplete { // выполняется в потоке ec
case Success(value) ⇒
println(value)
case Failure(ex) ⇒
println(s"Failed: ${ex.toString}")
}
всё еще не ФП
import scala.concurrent.{Await, Future}
import scala.concurrent.duration._
val f: Future[Int] = ???
val result: Int = Await.result(f, 5 minutes)
блокирует текущий поток до получения результата
import scala.concurrent.Future
import scala.util.{Failure, Success}
val f: Future[Int] = ???
f.value match {
case Some(Success(value)) ⇒
println(value)
case Some(Failure(ex)) ⇒
throw ex
case None ⇒
println("Not completed :-(")
}
Promise - контейнер для результата отложенного вычисления.
Нужен для построения асинхронных функций.
Promise = контейнер + связанная с ним Future
def func(input: String, onComplete: Try[String] => Unit): Unit
def futureFunc(input: String): Future[String] = {
val p: Promise[String] = Promise()
func(input, p.complete)
p.future
}
def func(input: String): String = ???
// apply[T](body: => T)(implicit ec: ExecutionContext)
def futureFunc(input: String): Future[String] = Future {
func(input)
}
Это удобная утилита, а не основное предназначение Future
// реализация в библиотеке сложнее
def run[T](f :=> T)
(implicit ec: ExecutionContext): Future[T] = {
val p = Promise[T]()
ec.execute(() ⇒ {
p.complete(Try(f))
})
p.future
}
val f: Future[Int] = run { 2 * 2 }
перешли от execute к более функциональному стилю
Иногда хотим поместить готовое значение в Future:
(ExecutionContext не нужен, поток не требуется)
Future - функтор, у нее есть функция map
источник: Functors and Applicatives
(Подробнее о функторах на лекции про Cats)
val f3: Future[Int] = f.map(_ * 10)
// возвращается мгновенно, вычисляется когда f будет вычислено
похоже на работу с ленивыми структурами данных
// pimp my library
implicit class MyFuture[T](val f: Future[T]) extends AnyVal {
def myMap[R](func: T ⇒ R)
(implicit ec: ExecutionContext): Future[R] = {
val p = Promise[R]()
f.onComplete {
case Success(v) ⇒ p.complete(Try(func(v)))
case Failure(ex) ⇒ p.failure(ex)
}
p.future
}
}
Future - монада
flatMap[S](f: T => Future[S])
(implicit ec: ExecutionContext): Future[S]
f - монадическая функция
def userByEmail(email: String): Future[Int] = ???
def ticketsByUser(user: Int): Future[Seq[Int]] = ???
// где-то должен быть implicit ec
def countTickets(email: String): Future[Int] =
userByEmail(email).flatMap(ticketsByUser).map(_.length)
функция возвращается мгновенно
Можно использовать for:
// где-то должен быть implicit ec
val count: Future[Int] = for {
user <- userByEmail("user1@test")
tickets <- ticketsByUser(user)
} yield {
tickets.length
}
аналог на callback'ах - "лапша",
особенно после добавления обработки ошибок
Еще полезные методы
(аналог map/flatMap, но с обработкой ошибок)
Избегайте в коде таких типов как Future[Future[...]], Future[Try[...]] и Try[Future[...]].
Это источник проблем.
Сборка независимых Future
val res: Future[Result] = for {
info <- getUserInfo(user)
stats <- getUserStat(user)
} yield Result(info, stats)
проблема - задержка вызова getUserStat
val infoF = getUserInfo(user)
val statsF = getUserStat(user)
val res: Future[Result] = for {
info <- infoF
stats <- statsF
} yield Result(info, stats)
Монада - абстракция цепочки вычислений;
тут не подходит
Future это аппликативный функтор, но об это на лекции про Cats
Используем zip
val infoF = getUserInfo(user)
getUserInfo(user)
.zip(getUserStat(user)) // Future[(info, stat)]
.map(Result.tupled _)
Future.sequence для списков
def process(i: Int): Future[Int] = ???
val processed: Seq[Future[Int]] =
Seq(1, 2, 3).map(process) // тут начинается работа
val completed: Future[Seq[Int]] =
Future.sequence(processed) // тут собираем результат
sequence может быть опасен - загружает очередь пула потоков и может "перегрузить" process и пул
Что плохого в Future?
Всё еще не ФП?
val x = Future(r.nextInt)
(x, x)
// vs
(Future(r.nextInt), Future(r.nextInt))
Что делать с Future[Option[T]]?
Пример map:
val maybeF = Future.successful(Some("value"))
maybeF.map(_.map(_.length))
Универсальный трансформер для функторов
import cats.data._
import cats.implicits._
// еще нужен implicit ec
val maybeF: Future[Option[String]] = ???
// заменяет maybeF.map(_.map(_.length))
Nested(maybeF).map(_.length).value
// возвращает Future[Option[Int]]
Можно ли соединить две разные монады?
В общем случае нет.
Что мы хотим от Future[Option[T]]?
можно сделать такой flatMap:
class FutureO[+A](val fut: Future[Option[A]]) extends AnyVal {
def flatMap[B](f: A ⇒ FutureO[B])
(implicit ec: ExecutionContext): FutureO[B] = {
val newFuture = fut.flatMap {
case Some(a) ⇒ f(a).fut
case None ⇒ Future.successful(None)
}
new FutureO(newFuture)
}
}
тут логика Option. От Future - flatMap и конструктор
На месте Future может быть любая другая монада.
Можно обобщить
(если у нас есть абстракция монады)
OptionT - создает монаду для M[Option[T]] (cats)
val greetingFO: Future[Option[String]] = ???
val firstnameF: Future[String] = ???
val lastnameO: Option[String] = ???
val ot = for { // OptionT[Future, String]
g <- OptionT(greetingFO)
f <- OptionT.liftF(firstnameF)
l <- OptionT.fromOption[Future](lastnameO)
} yield s"$g $f $l"
val result: Future[Option[String]] = ot.value
Трансформеры есть и для некоторых других монад.
Напоминаю: