Skip to content

Commit f0c4085

Browse files
two more subparts
1 parent b37b224 commit f0c4085

File tree

2 files changed

+172
-0
lines changed

2 files changed

+172
-0
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
layout: overview-large
3+
title: Преобразования параллельных коллекций
4+
5+
disqus: true
6+
7+
partof: parallel-collections
8+
language: ru
9+
num: 3
10+
---
11+
12+
## Взаимные преобразования последовательных и параллельных коллекций
13+
14+
Любая последовательная коллекция может быть преобразована в свою параллельную альтернативу вызовом метода `par`, причем некоторые типы последовательных коллекций имеют прямой параллельный аналог. Для таких коллекций конвертация эффективна-- она занимает постоянное время, так как и последовательная и параллельная коллекция представлены одной и той же структурой данных (за исключением изменяемых хэш-таблиц и хэш-множеств, преобразование которых требует больше времени в первый вызов метода `par`, тогда как последующие вызовы `par` занимают постоянное время). Нужно заметить, что если изменяемые коллекции делят одну лежащую в основе структуру данных, то изменения, сделанные в последовательной коллекции, будут видны в ее параллельной ответной части.
15+
16+
| Последовательные | Параллельные |
17+
| ---------------- | -------------- |
18+
| **изменяемые** | |
19+
| `Array` | `ParArray` |
20+
| `HashMap` | `ParHashMap` |
21+
| `HashSet` | `ParHashSet` |
22+
| `TrieMap` | `ParTrieMap` |
23+
| **неизменяемые** | |
24+
| `Vector` | `ParVector` |
25+
| `Range` | `ParRange` |
26+
| `HashMap` | `ParHashMap` |
27+
| `HashSet` | `ParHashSet` |
28+
29+
Другие коллекции, такие, как списки, очереди или потоки, последовательны по своей сути, в том смысле, что элементы должны выбираться один за другим. Такие коллекции преобразуются в свои параллельные альтернативы копированием элементов в схожую параллельную коллекцию. Например, рекурсивный список (functional list) преобразуется в стандартную неизменяемую параллельную последовательность, то есть в параллельный вектор.
30+
31+
Любая параллельная коллекция может быть преобразована в её последовательный вариант вызовом метода `seq`. Конвертирование параллельной коллекции в последовательную эффективно всегда-- оно занимает постоянное время. Вызов `seq` на изменяемой параллельной коллекции возвращает последовательную, которая отображает ту же область памяти-- изменения, сделанные в одной коллекции, будут видимы в другой.
32+
33+
## Преобразования между различными типами коллекций
34+
35+
Параллельные коллекции могут конвертироваться в другие типы коллекций, не теряя при этом своей параллельности. Например, вызов метода `toSeq` последовательное множество преобразует в обычную последовательность, а параллельное-- в параллельную. Общий принцип такой: если есть параллельный вариант коллекции `X`, то метод `toX` преобразует коллекцию к типу `ParX`.
36+
37+
Ниже приведена сводная таблица всех методов преобразования:
38+
39+
| Метод | Тип возвращаемого значения |
40+
| -------------- | -------------------------- |
41+
| `toArray` | `Array` |
42+
| `toList` | `List` |
43+
| `toIndexedSeq` | `IndexedSeq` |
44+
| `toStream` | `Stream` |
45+
| `toIterator` | `Iterator` |
46+
| `toBuffer` | `Buffer` |
47+
| `toTraversable`| `GenTraverable` |
48+
| `toIterable` | `ParIterable` |
49+
| `toSeq` | `ParSeq` |
50+
| `toSet` | `ParSet` |
51+
| `toMap` | `ParMap` |
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
---
2+
layout: overview-large
3+
title: Многопоточные нагруженные деревья
4+
5+
disqus: true
6+
7+
partof: parallel-collections
8+
language: ru
9+
num: 4
10+
---
11+
12+
Большинство многопоточных структур данных не гарантирует правильности последовательного перебора элементов в случае, если эта структура изменяется во время прохождения. То же верно, кстати, и в случае большинства изменяемых коллекций. Особенность многопоточных нагруженных деревьев (также известных, как префиксные деревья) -- `tries`-- заключается в том, что они позволяют модифицировать само дерево, которое в данный момент просматривается. Сделанные изменения становятся видимыми только при следующем прохождении. Так ведут себя и последовательные нагруженные деревья, и их параллельные аналоги; единственное отличие-- в том, что первые перебирают элементы последовательно, а вторые-- параллельно.
13+
14+
Это замечательное свойство позволяет упростить ряд алгоритмов. Обычно это такие алгоритмы, в которых некоторый набор данных обрабатывается итеративно, причем для обработки различных элементов требуется различное количество итераций.
15+
16+
В следующем примере вычисляются квадратные корни некоторого набор чисел. Каждая итерация обновляет значение квадратного корня. Числа, квадратные корни которых достигли необходимой точности, исключаются из перебираемого набора.
17+
18+
case class Entry(num: Double) {
19+
var sqrt = num
20+
}
21+
22+
val length = 50000
23+
24+
// готовим исходные данные
25+
val entries = (1 until length) map { num => Entry(num.toDouble) }
26+
val results = ParTrieMap()
27+
for (e <- entries) results += ((e.num, e))
28+
29+
// вычисляем квадратные корни
30+
while (results.nonEmpty) {
31+
for ((num, e) <- results) {
32+
val nsqrt = 0.5 * (e.sqrt + e.num / e.sqrt)
33+
if (math.abs(nsqrt - e.sqrt) < 0.01) {
34+
results.remove(num)
35+
} else e.sqrt = nsqrt
36+
}
37+
}
38+
39+
Отметим, что в приведенном выше вычислении квадратных корней вавилонским методом (\[[3][3]\]) некоторые значения могут сойтись гораздо быстрее, чем остальные. По этой причине мы исключаем их из `results`, чтобы перебирались только те элементы, которые нуждаются в дальнейшей обработке.
40+
41+
Другим примером является алгоритм поиска в ширину, который итеративно расширяет очередь перебираемых узлов до тех пор, пока или не будет найден целевой узел, или не закончатся узлы, за счет которых можно расширить поиск. Определим точку на двухмерной карте как кортеж значений `Int`. Обозначим как `map` двухмерный массив булевых значений, которые обозначают, занята соответствующая ячейка или нет. Затем объявим два многопоточных дерева-- `open`, которое содержит все точки, которые требуется раскрыть, и `closed`, в котором хранятся уже обработанные точки. Мы намерены начать поиск с углов карты и найти путь к центру-- инициализируем ассоциативный массив `open` подходящими точками. Затем будем раскрывать параллельно все точки, содержащиеся в ассоциативном массиве `open` до тех пор, пока больше не останется точек. Каждый раз, когда точка раскрывается, она удаляется из массива `open` и помещается в массив `closed`.
42+
43+
Выполнив все это, выведем путь от целевого до стартового узла.
44+
45+
val length = 1000
46+
47+
// объявляем тип Node
48+
type Node = (Int, Int);
49+
type Parent = (Int, Int);
50+
51+
// операции над типом Node
52+
def up(n: Node) = (n._1, n._2 - 1);
53+
def down(n: Node) = (n._1, n._2 + 1);
54+
def left(n: Node) = (n._1 - 1, n._2);
55+
def right(n: Node) = (n._1 + 1, n._2);
56+
57+
// создаем карту и целевую точку
58+
val target = (length / 2, length / 2);
59+
val map = Array.tabulate(length, length)((x, y) => (x % 3) != 0 || (y % 3) != 0 || (x, y) == target)
60+
def onMap(n: Node) = n._1 >= 0 && n._1 < length && n._2 >= 0 && n._2 < length
61+
62+
// список open - фронт обработки
63+
// список closed - уже обработанные точки
64+
val open = ParTrieMap[Node, Parent]()
65+
val closed = ParTrieMap[Node, Parent]()
66+
67+
// добавляем несколько стартовых позиций
68+
open((0, 0)) = null
69+
open((length - 1, length - 1)) = null
70+
open((0, length - 1)) = null
71+
open((length - 1, 0)) = null
72+
73+
// "жадный" поиск в ширину
74+
while (open.nonEmpty && !open.contains(target)) {
75+
for ((node, parent) <- open) {
76+
def expand(next: Node) {
77+
if (onMap(next) && map(next._1)(next._2) && !closed.contains(next) && !open.contains(next)) {
78+
open(next) = node
79+
}
80+
}
81+
expand(up(node))
82+
expand(down(node))
83+
expand(left(node))
84+
expand(right(node))
85+
closed(node) = parent
86+
open.remove(node)
87+
}
88+
}
89+
90+
// выводим путь
91+
var pathnode = open(target)
92+
while (closed.contains(pathnode)) {
93+
print(pathnode + "->")
94+
pathnode = closed(pathnode)
95+
}
96+
println()
97+
98+
На GitHub есть пример реализации игры "Жизнь", который использует многопоточные хэш-деревья-- `Ctries`, чтобы выборочно симулировать только те части механизма игры, которые в настоящий момент активны \[[4][4]\].
99+
Он также включает в себя основанную на `Swing` визуализацию, которая позволяет посмотреть, как подстройка параметров влияет на производительность.
100+
101+
Многопоточные нагруженные деревья также поддерживают атомарную, неблокирующую операцию `snapshot`, выполнение которой осуществляется за постоянное время. Эта операция создает новое многопоточное дерево со всеми элементами на некоторый выбранный момент времени, создавая таким образом снимок состояния дерева в этот момент.
102+
На самом деле, операция `snapshot` просто создает новый корень дерева. Последующие изменения отложенно перестраивают ту часть многопоточного дерева, которая соответствует изменению, и оставляет нетронутой ту часть, которая не изменилась. Прежде всего это означает, что операция 'snapshot' сама по себе не затратна, так как не происходит копирования элементов. Кроме того, так как оптимизация "копирования при записи" создает копии только измененных частей дерева, последующие модификации горизонтально масштабируемы.
103+
Метод `readOnlySnapshot` чуть более эффективен, чем метод `snapshot`, но он возвращает неизменяемый ассоциативный массив, который доступен только для чтения. Многопоточные деревья также поддерживают атомарную операцию постоянного времени `clear`, основанную на рассмотренном механизме снимков.
104+
Чтобы подробнее узнать о том, как работают многопоточные деревья и их снимки, смотрите \[[1][1]\] и \[[2][2]\].
105+
106+
На рассмотренном механизме снимков основана работа итераторов многопоточных деревьев. Прежде чем будет создан объект-итератор, берется снимок многопоточного дерева. Таким образом, итератор перебирает только те элементы дерева, которые присутствовали на момент создания снимка. Фактически, итераторы используют те снимки, которые дают доступ только на чтение.
107+
108+
На том же механизме снимков основана операция `size`. В качестве примитивной реализации этой операции можно просто создать итератор (то есть, снимок) и перебрать все элементы, подсчитывая их. Таким образом, каждый вызов операции `size` будет требовать времени, прямо пропорционального числу элементов. Однако, многопоточные деревья в целях оптимизации кэшируют размеры своих отдельных частей, тем самым уменьшая временную сложность метода `size` до амортизированно-логарифмической. В результате получается, что если вызвать метод `size` один раз, можно осуществлять последующие вызовы `size` затрачивая минимум ресурсов, вычисляя, как правило, размеры только тех частей, которые изменились после последнего вызова `size`. Кроме того, вычисление размера параллельных многопоточных деревьев выполняется параллельно.
109+
110+
111+
## Ссылки
112+
113+
1. ["Cache-Aware" неблокирующие многопоточные хэш-деревья][1]
114+
2. [Многопоточные деревья, поддерживающие эффективные неблокирующие снимки][2]
115+
3. [Методы вычисления квадратных корней][3]
116+
4. [Симуляция игры "Жизнь"][4]
117+
118+
[1]: http://infoscience.epfl.ch/record/166908/files/ctries-techreport.pdf "Ctries-techreport"
119+
[2]: http://lampwww.epfl.ch/~prokopec/ctries-snapshot.pdf "Ctries-snapshot"
120+
[3]: http://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method "babylonian-method"
121+
[4]: https://github.com/axel22/ScalaDays2012-TrieMap "game-of-life-ctries"

0 commit comments

Comments
 (0)