-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathlecture13_concurrency.tex
375 lines (351 loc) · 19.8 KB
/
lecture13_concurrency.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
% !TeX document-id = {efee450f-6069-4a1d-a54f-d3ae0eb7270d}
% !TeX TXS-program:compile = txs:///pdflatex/[-shell-escape]
\documentclass[11pt]{beamer}
\input{lecture_preamble.tex}
\title{Лекция 13: многопоточность}
\begin{document}
\begin{frame}[plain]
\maketitle
\end{frame}
\begin{frame}[fragile]
\frametitle{Два вида многопоточности: конкурентность и параллелизм}
\begin{itemize}
\item Все программы, которые мы писали до сих пор, были однопоточными.
\item У современных компьютеров много ядер, мы задействуем только одно.
\item Чтобы использовать их, нужна многопоточность. \pause
\item Параллелизм: физическая многопоточность. Много ядер (или других ресурсов) используются для ускорения выполнения одной задачи. \pause
\item Конкурентность: логическая многопоточность. Несколько задач, которые могут выполняться логически одновременно. А значит, потенциально и физически одновременно.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Детерминированность}
\begin{itemize}
\item Модель программирования детерминирована, если результат программы зависит только от полученных данных (аргументов и ввода пользователя). \pause
\item Недетерминирована, если результат зависит от того, в каком порядке произойдут события (или от сгенерированных случайных чисел и т.д.). \pause
\item Обычно параллельные программы детерминированы, конкурентные нет.
\item Недетерминированность усложняет тестирование, отладку и понимание программ, но часто без неё не обойтись.
\item Тестирование свойств это пример пользы от недетерминированности.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Параллелизм: монада \haskinline|Eval|}
\begin{itemize}
\item Модуль \haskinline|Concurrent.Parallel.Strategies| в пакете \hackage{parallel}.
\item Тип \haskinline|Eval a| --- монада, где в \haskinline|mx >>= f| значение \haskinline|mx| будет вычислено до СЗНФ перед запуском \haskinline|f|. \pause
\item Есть функции
\begin{haskell}
runEval :: Eval a -> a
rpar :: a -> Eval a
rseq :: a -> Eval a
parEval :: Eval a -> Eval a
\end{haskell}
\item \haskinline|rpar x|: запустить вычисление \haskinline|x| и сразу вернуться.
\item \haskinline|rseq x|: вернуться после вычисления \haskinline|x|.
\item \haskinline|parEval x|: запустить вычисление, описанное в \haskinline|x|, и сразу вернуться.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Примеры \haskinline|Eval|}
\begin{itemize}
\item
Запуск двух вычислений параллельно:
\begin{haskell}
runEval $ do
a <- rpar (f x)
b <- rpar (f y)
return (a, b)
\end{haskell}
% \includegraphics[width=\textwidth]{concurrency_eval1.png}
\pause
\item
Запуск двух вычислений параллельно и возврат, когда оба закончатся:
\begin{haskell}
runEval $ do
a <- rpar (f x)
b <- rseq (f y)
rseq a
return (a, b)
\end{haskell}
% \includegraphics[width=\textwidth]{concurrency_eval2.png}
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Фибоначчи в \haskinline|Eval|}
\begin{itemize}
\item
\begin{haskellsmall}
parFib1 :: Integer -> Integer
parFib1 0 = 1
parFib1 1 = 1
parFib1 n = runEval $ do !\pause!
f1 <- rpar (parFib1 (n - 1))
f2 <- rseq (parFib1 (n - 2))
-- rseq, чтобы занять текущий поток, можно и rpar
pure (f1 + f2)
\end{haskellsmall}
\pause
\item
\begin{haskellsmall}
parFib2 :: Integer -> Eval Integer
parFib2 0 = pure 1
parFib2 1 = pure 1
parFib2 n = do !\pause!
f1 <- parEval (parFib2 (n - 1))
f2 <- parFib2 (n - 2)
pure (f1 + f2)
\end{haskellsmall}
\pause
\item Здесь для простоты кода много повторных вычислений!
\end{itemize}
\end{frame}
% TODO восстановить слайд
%\begin{frame}[fragile]
%\frametitle{Примеры \haskinline|Eval|}
%\begin{columns}
% \begin{column}{0.5\textwidth}
% Запуск двух вычислений параллельно:
%\begin{haskell}
%runEval $ do
% a <- rpar (f x)
% b <- rpar (f y)
% return (a, b)
%\end{haskell}
%% \includegraphics[width=\textwidth]{concurrency_eval1.png}
% \end{column}\pause
% \begin{column}{0.5\textwidth}
% Запуск двух вычислений параллельно и возврат, когда оба закончатся:
%\begin{haskell}
%runEval $ do
% a <- rpar (f x)
% b <- rseq (f y)
% rseq a
% return (a, b)
%\end{haskell}
%% \includegraphics[width=\textwidth]{concurrency_eval2.png}
% \end{column}
%\end{columns}
%\end{frame}
\begin{frame}[fragile]
\frametitle{Стратегии вычислений}
\begin{itemize}
\item \haskinline|type Strategy a = a -> Eval a|.
\item \haskinline|rpar| и \haskinline|rseq| --- стратегии.
\item \haskinline|r0| --- стратегия, которая ничего не вычисляет.
\item Стратегия принимает на вход thunk, вычисляет его части с помощью \haskinline|r0|, \haskinline|rpar| и \haskinline|rseq|.
\item Например, обобщая пример с прошлого слайда
\begin{haskell}
rparTuple2 :: Strategy (a, b)
rparTuple2 (a, b) = do !\pause!
a' <- rpar a
b' <- rpar b
return (a', b')
\end{haskell}
\pause
(или \haskinline[fontsize=\small]|rparTuple2 (a, b) = liftA2 (,) (rpar a) (rpar b)|)
\item
\begin{haskell}
withStrategy :: Strategy a -> a -> a
withStrategy s x = runEval (s x)
\end{haskell}
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Комбинаторы стратегий}
\begin{itemize}
\item Обобщим \haskinline|rparTuple2| дальше, чтобы она принимала стратегии на вход:
\begin{haskell}
evalTuple2 :: Strategy a -> Strategy b ->
Strategy (a, b)
evalTuple2 strat_a strat_b (a, b) = !\pause!
liftA2 (,) (strat_a a) (strat_b b)
\end{haskell}
\pause
\item \haskinline|rparWith :: Strategy a -> Strategy a| запускает стратегию параллельно.
\item Например, \haskinline|rparWith rpar| и \haskinline|rparWith rseq| эквивалентны \haskinline|rpar|.
\pause
\item
\begin{haskell}
parTuple2 :: Strategy a -> Strategy b ->
Strategy (a, b)
parTuple2 strat_a strat_b =
evalTuple2 (rparWith strat_a) (rparWith strat_b)
\end{haskell}
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Комбинаторы стратегий для списков}
\begin{itemize}
\item С помощью стратегий достаточно легко сделать параллельные \haskinline|map|/\haskinline|filter|/и т.д.
\item Например, используя \haskinline|parList :: Strategy a -> Strategy [a]|:
\begin{haskell}
parallelMap :: (a -> b) -> [a] -> [b]
parallelMap f xs =
withStrategy (parList rseq) (map f xs)
\end{haskell}
\pause
\item Это скорее всего создаст слишком много \enquote{искр}, лучше использовать
\begin{itemize}
\item \haskinline|parListChunk|: разбивает список на части одинаковой длины и работает параллельно с каждой частью
\item \haskinline|parBuffer|: начинает работать параллельно с первыми элементами списка, добавляет следующие при использовании результатов
\end{itemize}
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Монада \haskinline|Par|: параллелизм без ленивости}
\begin{itemize}
\item Модуль \haskinline|Control.Monad.Par| в пакете \haskinline|monad-par|.
\item Тип \haskinline|Par a| --- тоже монада, описывающая процесс вычисления значения типа \haskinline|a| с параллельными частями.
\item \haskinline|fork :: Par () -> Par ()| создаёт новый поток.
\item \haskinline|runPar :: Par a -> a| производит вычисление и возвращает его результат. \pause
\item Потоки общаются между собой с помощью \haskinline|IVar|, которые можно записать только 1 раз.
\item В результате не должно быть \haskinline|IVar| или ссылок на них!
\item При соблюдении этого условия вычисления в \haskinline|Par| детерминированы.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{\haskinline|IVar|}
\begin{itemize}
\item Интерфейс \haskinline|IVar|:
\begin{haskell}
new :: Par (IVar a)
newFull :: NFData a => a -> Par (IVar a)
put :: NFData a => IVar a -> a -> Par ()
get :: IVar a -> Par a
\end{haskell}
\item Значения, положенные в \haskinline|IVar|, полностью вычисляются (есть ещё \haskinline|newFull_| и \haskinline|put_|).
\item Переменной можно дать значение только 1 раз, иначе исключение.
\item При вызове \haskinline|get| для переменной, ещё не имеющей значения, начинается ожидание.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Примеры \haskinline|Par|}
\begin{itemize}
\item \haskinline|spawn :: NFData a => Par a -> Par (IVar a)|: аналог \haskinline|fork|, но возвращающий переменную, в которой будет сохранено вычисленное значение
\begin{haskell}
spawn par = do
var <- new
fork $ do (x <- par; put var x)
return var
\end{haskell}
\item Параллельный \haskinline|map|:
\begin{haskell}
parallelMap :: NFData b => (a -> b) -> [a] -> [b]
parallelMap f xs = runPar $ do
ivars <- mapM (\x -> spawn (pure (f x)) xs
mapM get ivars
\end{haskell}
Как и в прошлый раз, создаёт \enquote{поток} для каждого элемента, для простоты кода.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Фибоначчи в \haskinline|Par|}
\begin{itemize}
\begin{haskell}
parFib :: Integer -> Par Integer
parFib 0 = pure 1
parFib 1 = pure 1
parFib n = do !\pause!
f1var <- spawn (parFib (n - 1))
f2 <- parFib (n - 2)
f1 <- get f1var
pure (f1 + f2)
\end{haskell}
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Конкурентность}
\begin{itemize}
\item Перейдём от параллелизма к конкурентности.
\item Модуль \haskinline|Control.Concurrent| в пакете \haskinline|base| и \haskinline|C.C.MVar|/\haskinline|C.C.Chan|/\haskinline|C.C.QSem[N]|.
\item \haskinline|ThreadId| --- ссылка на поток.
\item \haskinline|forkIO :: IO () -> IO ThreadId| создаёт новый поток, который произведёт данные действия и выйдет, возвращает ссылку на созданный поток.
\pause
\item Это так называемый \enquote{зелёный поток}, то есть не поток операционной системы, а похожий по поведению объект виртуальной машины Haskell.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{\haskinline|MVar|}
\begin{itemize}
\item Потоки могут взаимодействать через \haskinline|MVar|, которые очень похожи на \haskinline|IVar|, но записанное значение можно убрать или изменить.
\item Переменная в любой момент или пустая или полная (содержит значение).
\begin{haskellsmall}
newEmptyMVar :: IO (MVar a)
newMVar :: a -> IO (MVar a)
putMVar :: MVar a -> a -> IO ()
readMVar :: MVar a -> IO a
takeMVar :: MVar a -> IO a
isEmptyMVar :: MVar a -> IO Bool
\end{haskellsmall}
\item Если \haskinline|putMVar| вызвана на полной переменной, она ждёт, пока та не опустеет.
\item \haskinline|takeMVar| и \haskinline|readMVar| отличаются тем, что первая убирает прочитанное значение. На пустой переменной обе ждут.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Фибоначчи с \haskinline|MVar|}
\begin{itemize}
\item Реализация вычисления чисел Фибоначчи с помощью \haskinline|MVar| не сильно отличается от \haskinline|Par| (только аналога \haskinline|spawn| в стандартной библиотеке нет, смотрите пакет \haskinline|async|):
\begin{haskell}
parFib :: Integer -> IO Integer
parFib 0 = pure 1
parFib 1 = pure 1
parFib n = do !\pause!
f1var <- newEmptyMVar
forkIO $ do
f1' <- parFib (n - 1)
putMVar f1var f1'
f2 <- parFib (n - 2)
f1 <- readMVar f1var
pure (f1 + f2)
\end{haskell}
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Правильный Фибоначчи}
\begin{itemize}
\item Уже упоминалось, что приведённые программы для вычисления чисел Фибоначчи имеют серьёзный недостаток.
\item Например, \haskinline|parFib 4| вызовет параллельно \haskinline|parFib 3| и \haskinline|parFib 2|, а \haskinline|parFib 3| снова вызовет \haskinline|parFib 2|.
\item Чтобы этого избежать, нужно добавить вспомогательный аргумент для хранения уже полученных результатов.
\item Его тип может быть:
\begin{itemize}
\item Для \haskinline|Eval|: \haskinline|Map Integer Integer|.
\item Для \haskinline|Par|: \haskinline|Map Integer (IVar Integer)|.
\item Для \haskinline|IO|: \haskinline|MVar (Map Integer Integer)|, хотя \haskinline|Map Integer (MVar Integer)| тоже подойдёт.
\end{itemize}
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{\haskinline|Chan|}
\begin{itemize}
\item \haskinline|MVar| можно рассматривать как каналы, хранящие не более одного значения.
\item \haskinline|Chan| это канал для произвольного количества значений.
\begin{haskell}
newChan :: IO (Chan a)
writeChan :: Chan a -> a -> IO ()
readChan :: Chan a -> IO a
\end{haskell}
\item \haskinline|writeChan| сохраняет значение в конец очереди.
\item \haskinline|readChan| читает из её начала и ждёт, если она пуста.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Транзакции}
\begin{itemize}
\item Понятие транзакций может быть знакомо по базам данных.
\item Это наборы операций, которые все вместе либо выполняются успешно, либо откатываются.
\item В Haskell это позволяет делать монада \haskinline|STM| с операциями над \haskinline|TVar|.
\item Типы гарантируют, что у действия в монаде \haskinline|STM| не может быть побочных эффектов, кроме изменения значений \haskinline|TVar|.
\item Обычно проще написать правильный код, который работает с несколькими \haskinline|TVar|, чем с \haskinline|MVar|, так как можно не беспокоиться о видимости промежуточных состояний.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Дополнительное чтение}
\begin{itemize}
\item \href{https://simonmar.github.io/pages/pcph.html}{Parallel and Concurrent Programming in Haskell} (книга, 2013, доступна бесплатно)
\item \href{https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/parallel_haskell2.pdf}{A Tutorial on Parallel and Concurrent Programming in Haskell}
\item \href{https://erlang.org/}{Erlang} Функциональный язык, построенный на модели акторов (взаимодействующих только через отправку сообщений друг другу вместо общих переменных).
\item \href{https://stackoverflow.com/questions/23326920/difference-between-par-monad-and-eval-monad-with-deepseq/23428610#23428610}{Ответ на Stack Overflow про разницу между \haskinline|Eval| и \haskinline|Par|} (ещё один в конце главы 4 первой ссылки).
\item \href{https://hackage.haskell.org/package/streamly}{Streamly: Beautiful Streaming, Concurrent and Reactive Composition}
\item \href{https://hackage.haskell.org/package/lvish}{lvish} Пакет, обобщающий \haskinline|Par| на основе теории решёток. К сожалению, мёртв с 2014.
\end{itemize}
\end{frame}
\end{document}