Досега?
- Програми с мощността на ламбда смятането/машината на Тюринг
- Последователни изчисления, не се влияят от времето
- Нямат връзка с околния свят
- вход => предвидима трансформация => изход
- Трансформиращи програми
- Добре изучени
IO
- Връзка с външния свят
- Но синхронна – програмата не прави нищо друго докато чака
- Интерактивни програми
Реалният свят
- Светът навън е силно паралелен и конкурентен
- Нещо повече, участниците в него си взаимодействат
- Развива се във времето
- Как да моделираме такива програми?
Конкурентност и паралелизъм
-
parallel
-
from παρά + ἄλληλος, along each other
-
concurrent
-
present active participle of concurrō (“happen at the same time with”), from con- (“with”) + currō (“run”)
-
concurrent computing
-
a form of computing in which several computations are executed during overlapping time periods—concurrently—instead of sequentially
Дистрибутираност
Независими компоненти със собствени изчислителни ресурси и памет, които комуникират помежду си чрез съобщения и изграждат цялостна система
Реактивност
Свойството на програмите/компонентите да реагират
на света около тях (с което да са част от него)
Конкурентност
В изчислителен контекст:
- конкурентността се отнася към структурата на програмата,
- паралелизмът, дистрибутираността – към хардуера и как тя ще бъде изпълнявана.
Конкурентните програми са композитност от unit-и от изчисления, които, веднъж дефинирани, могат да бъдат изпълнени независимо едно от друго.
Конкурентни модели
- Нишки
- I/O и TCP/IP конкурентност
- Callbacks
- Event loop
- Future и Promise
- Конкурентни опашки
- Агенти
- Актьорски модел
- Communicating Sequential Processes
- Software Transactional Memory
- Stream/dataflow конкурентност
- …
Какво прави един модел подходящ?
- Лесен за проследяване и за разсъждаване върху твърдения за него
- Предоставя изразни средства, с които да решим проблемът, който моделираме, но ни предпазва от сложността на по-общи проблеми
- Не скрива особеностите на света на проблема – недетерминизъм, възможност за грешки, латентност и други
- Предпазва ни от състояния, до които не искаме да стигаме, ако не са в домейна на проблема – race conditions, недетерминизъм и други
- Композитен
- Минимизира недетерминистичното влияние на времето
- Functional programming to the rescue
Комуникация между конкурентни примитиви
- За да бъде смислен, всеки конкурентен примитив е нужно да има поне една интеракция с околния свят или с други примитиви
- Границите с дистрибутираните системи се размиват
- Нишки – чрез споделена памет и средства на процесора и ОС
Happens-before
- Запис във
volatile
променлива се случва преди последващо нейно прочитане (от същата или друга нишка).
- За всеки два последователни statement-а в една нишка, първият се случва преди вторият.
- Релацията е транзитивно затворена.
- Happens-before образува частична наредба.
- Стартирането на нишка се случва преди всеки statement от стартираната нишка.
Happens-before
JVM ни гарантира, че всяка референция към immutable обект сочи към обект с напълно валидно състояние.
Затова неизменямостта улеснява reasoning-а значително и премахва огромен клас от възможни грешки при конкурентни програми.
Какво прави един модел подходящ?
- Лесен за проследяване и за разсъждаване върху твърдения за него
- Предоставя изразни средства, с които да решим проблемът, който моделираме, но ни предпазва от сложността на по-общи проблеми
- Не скрива особеностите на света на проблема – недетерминизъм, възможност за грешки, латентност и други
- Предпазва ни от състояния, до които не искаме да стигаме, ако не са в домейна на проблема – race conditions, недетерминизъм и други
- Композитен
- Минимизира недетерминистичното влияние на времето
Проблеми на нишките
- Тежки – всяка има стек, регистри, превключването е бавно и минава през ядрото на ОС, стартират бавно
- Трудни за проследяване – недетерминизъм е възможен във всяка една част, споделяща данни с други нишки
- Фин контрол на недетерминизма – чрез мютекси и други средства за синхронизация (race conditions, deadlocks)
- Некомпозитни, не поддържат трансформации
- Всяка последваща задача трябва да бъде създадена императивно от самата нишка
- Синхронност срещу асинхронност
- The Problem with Threads
Предния път
- Програми, взаимодействащи с външния свят
- Конкурентност, паралелализъм, дистрибутираност, реактивност
- Конкурентни модели. Какво прави един модел добър?
- Нишки. Комуникация между нишки (и happens-before)
- Immutability – гарантираност за валидно състояние
Конкурентните програми са композитност от изчислителни примитиви (unit-и), които, веднъж дефинирани, могат да бъдат изпълнени независимо едно от друго
Конкурентни изчислителни примитиви
- Колко да са големи, колко дълго да живеят?
- Как други примитиви могат да реагират на тяхно действие или на тяхното завършване (реактивност)?
- Как да си говорят едни с други?
- Как да се композират?
Нишките
- Липсва реактивност
- но могат активно да създадат други нишки
- и да променят споделено състояние
- Комуникират
- чрез споделено състояние
- чрез синхронни и блокиращи операции – докато операцията не завърши нишката не може да прави нищо друго
- Не се композират
Callbacks?
- Асинхронни и реактивни
- Задействат се при завършване на работа или при определено събитие (естествени за event loop)
- Ще ги изпълним върху pool от нишки (брой = ~брой ядра)
Callbacks – негативи
- Императивни, работят с mutable state
- Некомпозитни. Callback hell
- Ако се изпълняват в различни нишки, изискват синхронизация
- Ръчно спряване с грешки
Future
Какво бяхме постигнали с IO
?
- Страничен ефект => функционален ефект
- Който можем да третираме като стойност
- Функционално композиране на ефекти (по определена операция)
- Странични ефекти само при изпълнение
Императивно срещу функционалнo
- Императивните програми
- описват постъпкови промени във времето
- всяка стъпка зависи от всички предишни
- всяка нишка е императивна програма със собствена времева линия
- споделената памет води до преплитане на много времеви линии
- Функционалните програми
- са изградени от изрази
- изразите описват зависимости. По декларативен начин
- резултатът на един израз зависи от неговите операнди, но самите операнди са независими
- те могат да се изчислят паралелно
- композиция на изрази/функции описва зависимост. Тя се изчислява последователно
- редът на дефинициите няма значение. В какъвто и ред да се изпълнят изразите, стига да се спазят зависимостите, ще се получи един и същи резултат
Eager vs lazy
Future
се изчислява асинхронно => допустимо е да се изчисляват eagerly
- т.е. изчислението да започне веднага след дефинирането и когато станат готови всички зависимости
- Ще разгледаме и двата варианта
Реактивност на Future
Future
комуникира с други Future
-и
- при стартиране (стартира когато неговите зависимости са готови)
- и при завършване (“уведомява” зависимите от него)
- Как да го вържем с други източници на събития?
- Promise
- Promise-ите ще генерират първоначални Future-и в нашата система
- Адаптер към външния свят, например позволяват реактивност към събития за вход/изход
- Ще ги скриваме за функционален интерфейс
Immutability
Future е безопасен, само ако стойностите в него са immutable!
Ако не са, то тяхното състояние може да е неизвестно
Future
в Scala
scala.concurrent.Future
- Използва
ExecutionContext
вместо Executor
- default:
import scala.concurrent.ExecutionContext.Implicits.global
Съществуваща стойност към Future
Recover with друга, по-стабилна алтернатива
Неопределен брой независими изчисления
Асинхронност срещу синхронност
- И двете имат и ползи и недостатъци
- Подходящи за различни случаи
- Асинхронността позволява конкурентните примитиви да продължат работа с нещо друго
- Позволява гъвкавост, код, описващ зависимости и лесно паралелизиране
- За разлика от синхронността, не създава строга зависимост между два компонента
- По-точно описва физическия свят
- По-голяма гъвкавост при спряване с недетерминизма от физическия свят (напр. загуба на съобщения)
- Синхронността кара синхронните примитиви да изчакват резултат
- По-лесно за проследяване
- Тъй като не се случват няколко неща едновременно, най-много едно
- Изисква допълнителни ресурси, но при подходяща имплементация носи гарантиран throughput (например чрез синхронен clock)
Асинхронен вход/изход чрез Future
Task – lazy Future
Ще разгледаме Task
имплементацията на Monix
Task – създаване
- Task.now (= Future.successful)
- Task.apply – за асинхронно изчисление (
Task { 1 + 1}
)
- Task.deferFuture
Task – композиране
- Task.zip2,…, Task.zipN
- Task.zipMap2,…, Task.zipMapN
- map, flatMap
- Task.sequence
- Task.chooseFirstOfList(List(a, b))
Task – изпълнение
val result: Future[Int] = Task(1 + 1).runAsync
Task(1 + 1).foreach(???)
- Нуждае се от
monix.execution.Scheduler
– подобно на ExecutionContext
Task – създаване (прод.)
- Task.evalOnce – мемоизация на резултата при последващи изпълнения
Task
- Подобно на IO, първо описаме план от трансформации
- Като всеки план, може да се изпълнява множество пъти
Актьорски модел
- Математически модел за конкурентни процеси, представен от Carl Hewitt през 1973-та
- Актьорите са универсиални изчислителни примитиви
- Те комуникират помежду си чрез съобщения
- Erlang независимо имплементира този модел през 80-те
Актьори
- Всеки актьор изпълнява функционална/Тюринг програма
- Всеки актьор си има “адрес”, на който могат да бъдат изпращани съобщения
- Програмата се нарича “поведение” и се задейства при получаване на съобщение
- Изходът от поведението, задействано от съобщението, съдържа:
- Поведението, което ще бъде изпълнено при следващото съобщение
- Списък от актьори, които да бъдат създадени, и начално поведение за тях
- Списък от съобщения и съответни адреси на получатели, които да бъдат изпратени
Актьори – недетерминизъм
- Поведението на актьора е детерминирано
- Всичко останало моделира недетерминизма на физическия свят. Съобщенията:
- могат да бъдат изгубени – няма гаранция, че ще стигнат до получателя
- могат да пристигнат в произволен ред
- пристигат след неопределено време
Актьорите като конкурентен примитив
- Комуникацията със съобщения е напълно асинхронна
- Те са реактивни от гледна точка на получаването на съобщения
- Трудни са за композиране (но може да се изградят композитни абстракции над тях)
- За сметка на по-реалистично моделиране на физическия свят
- Не споделят памет и не се нуждаят от средства за синхронизация. Всичко се оркестрира чрез комуникацията със съобщенията
- Съобщенията могат да бъдат трансформирани и обработвани чрез чисто програмни средства. Това позволява изграждането на произволни комуникационни топологии
- Могат да бъдат дистрибутирани