Один из основных мотивов добавления в Java 8 лямбда-выражений — упростить написание многопоточных программ. На примере несложной вычислительной задачи я покажу эволюцию средств Java для многопоточности. Начнём с Java Threads, а закончим лямбда-выражениями и Stream API. Ну и в результате посмотрим, что и как вышло.
4. The Star7 PDA
• SPARC based, handheld wireless PDA
• with a 5" color LCD with touchscreen
input
• a new 16 bit color hardware double
buffered NTSC framebuffer
• 900MHz wireless networking
• multi-media audio codec
• a new power supply/battery
interface
• a version of Unix (SolarisOs) that
runs in under a megabyte including
drivers for PCMCIA
• radio networking
• flash RAM file system
6. + Оак
• a new small, safe, secure, distributed, robust,
interpreted, garbage collected, multi-threaded,
architecture neutral, high performance,
dynamic programming language
• a set of classes that implement a spatial user
interface metaphor, a user interface
methodology which uses animation, audio,
spatial cues, gestures
• All of this, in 1992!
7. Зачем это все?
• Если Oak предназначался для подобных
устройств, когда еще было не особо много
многопроцессорных машин (и тем более
никто не мечтала о телефоне с 4 ядрами),
то зачем он изначально содержал
поддержку потоков???
8. Green Threads package
• The Green Threads
package totally manages
its own threads
• Green-threads Java
runtimes don't require
the underlying operating
systems to support
threads -- the runtime
handles scheduling,
preemption, and all
other thread-related
tasks all by itself
9.
10. Напишем реализации одной и той же
задачи с использованием
• Sequential algorithm
• Java Threads
• java.util.concurrent (Thread pool)
• Fork/Join
• Java 8 Stream API (Lambda)
11. А так же …
• Сравним производительность каждого из
подходов
30. Ограничения классического
подхода
• "поток-на-задачу" хорошо работает с небольшим
количеством долгосрочных задач
• слияние низкоуровневого кода, отвечающего за
многопоточное исполнение, и высокоуровневого
кода, отвечающего за основную функциональность
приложения приводит к т.н. «спагетти-коду»
• трудности связанные с управлением потоками
• поток занимает относительно много места в памяти
~ 1 Mb
• для выполнения новой задачи потребуется
запустить новый поток – это одна из самых
требовательных к ресурсам операций
33. Thread pool
• Пул потоков - это очередь в сочетании с
фиксированной группой рабочих потоков, в
которой используются wait() и notify(), чтобы
сигнализировать ожидающим потокам, что
прибыла новая работа.
34. Thread Pool Example
Executes the given task at some time in
the future.
The task may execute in a new thread, in a
pooled thread, or in the calling thread
35. class CalcThread implements Callable<Double> {
private final double start;
private final double end;
private final double step;
public CalcThread(double start, double end, double step) {
this.start = start;
this.end = end;
this.step = step;
}
@Override
public Double call() {
double partialResult = 0.0;
double x = start;
while (x < end) {
partialResult += step * func.apply(x);
x += step;
}
return partialResult;
}
}
36. public double calculate(double start, double end, double step,
int chunks) {
ExecutorService executorService =
Executors.newFixedThreadPool(chunks);
Future<Double>[] futures = new Future[chunks];
double interval = (end - start) / chunks;
double st = start;
for (int i = 0; i < chunks; i++) {
futures[i] = executorService.submit(
new CalcThread(st, st + interval, step));
st += interval;
}
executorService.shutdown();
double result = 0.0;
for (Future<Double> partRes : futures) {
result += partRes.get();
}
return result;
}
37. public double calculate(double start, double end, double step,
int chunks) {
ExecutorService executorService =
Executors.newFixedThreadPool(chunks);
Future<Double>[] futures = new Future[chunks];
double interval = (end - start) / chunks;
double st = start;
for (int i = 0; i < chunks; i++) {
futures[i] = executorService.submit(
new CalcThread(st, st + interval, step));
st += interval;
}
executorService.shutdown();
double result = 0.0;
for (Future<Double> partRes : futures) {
result += partRes.get();
}
return result;
}
Spliterator
Collector
38. 0
0.5
1
1.5
2
1 2 4 8 16 32
t(sec)
Threads
Execution time
Simple Threads
Thread Pool
41. «Бытие определяет сознание»
Доминирующие в текущий момент аппаратные
платформы формируют подход к созданию языков,
библиотек и систем
• С самого момента зарождения языка в Java была
поддержка потоков и параллелизма (Thread,
synchronized, volatile, …)
• Однако примитивы параллелизма, введенные в 1995
году, отражали реальность аппаратного обеспечения
того времени: большинство доступных коммерческих
систем вообще не предоставляли возможностей
использования параллелизма, и даже наиболее
дорогостоящие системы предоставляли такие
возможности лишь в ограниченных масштабах
• В те дни потоки использовались в основном, для
выражения asynchrony, а не concurrency, и в результате,
эти механизмы в целом отвечали требованиям времени
42. Путь к параллелизму
• По мере изменения доминирующей аппаратной платформы,
должна соответственно изменяться и программная платформа
• Когда начался процесс удешевления многопроцессорных
систем, от приложений стали требовать все большего
использования предоставляемого системами аппаратного
параллелизма. Тогда программисты обнаружили, что
разрабатывать параллельные программы, использующие
низкоуровневые примитивы, обеспечиваемые языком и
библиотекой классов, сложно и чревато ошибками
• java.util.concurrent дала возможности для «coarse-grained»
параллелизма (поток на запрос), но этого может быть не
достаточно, т.к. сам по себе запрос может выполняться долго
• Необходимы средства для «finer-grained» параллелизма
Web server
Th1 Th2 Th3 ThNcoarse-grained parallelism
finer-grained parallelism
43. Fork/Join
• Fork/Join сейчас является одной из самых
распространённых методик для построения
параллельных алгоритмов
Result solve(Problem problem) {
if (problem is small)
directly solve problem
else {
split problem into independent parts
fork new subtasks to solve each part
join all subtasks
compose result from subresults
}
}
44. ForkJoinExecutor
• ForkJoinExecutor подобен Executor, так как он
предназначен для запуска задач, однако он в
большей степени предназначен для
требующих интенсивных расчетов задач,
которые не блокируются
• ForkJoinPool, может в небольшом количестве
потоков выполнить существенно большее
число задач
• Это достигается путём так называемого work-
stealing'а (планировщики на основе захвата
работы ), когда спящая задача на самом деле
не спит, а выполняет другие задачи
46. Work stealing
• Планировщики на основе захвата работы (work
stealing) "автоматически" балансируют нагрузку за
счёт того, что потоки, оказавшиеся без задач,
самостоятельно обнаруживают и забирают
"свободные" задачи у других потоков. Находится ли
поток-"жертва" в активном или пассивном
состоянии, неважно.
• Основными преимуществами перед
планировщиком с общим пулом задач:
– отсутствие общего пула :), то есть точки глобальной
синхронизации
– лучшая локальность данных, потому что в большинстве
случаев поток самостоятельно выполняет
порождённые им задачи
47. Fork/Join effectiveness
• It is important to note that local task queues and work
stealing are only utilised (and therefore only produce
benefits) when worker threads actually schedule new
tasks in their own queues. If this doesn't occur, the
ForkJoinPool is just a ThreadPoolExecutor with an extra
overhead.
• If input tasks are already split (or are splittable) into
tasks of approximately equal computing load, then the
additional overhead of ForkJoinPool's splitting and
work stealing make it less efficient than just using a
ThreadPoolExecutor directly. But if tasks have variable
computing load and can be split into subtasks, then
ForkJoinPool's in-built load balancing is likely to make it
more efficient than using a ThreadPoolExecutor.
48. public class ForkJoinCalculate extends RecursiveTask<Double> {
...
static final long SEQUENTIAL_THRESHOLD = 500;
...
@Override
protected Double compute() {
if ((end - start) / step < SEQUENTIAL_THRESHOLD) {
return sequentialCompute();
}
double mid = start + (end - start) / 2.0;
ForkJoinCalculate left =
new ForkJoinCalculate(func, start, mid, step);
ForkJoinCalculate right =
new ForkJoinCalculate(func, mid, end, step);
left.fork();
double rightAns = right.compute();
double leftAns = left.join();
return leftAns + rightAns;
}
}
49. protected double sequentialCompute() {
double x = start;
double result = 0.0;
while (x < end) {
result += step * func.apply(x);
x += step;
}
return result;
}
50. Spliterator
public class ForkJoinCalculate extends RecursiveTask<Double> {
...
static final long SEQUENTIAL_THRESHOLD = 500;
...
@Override
protected Double compute() {
if ((end - start) / step < SEQUENTIAL_THRESHOLD) {
return sequentialCompute();
}
double mid = start + (end - start) / 2.0;
ForkJoinCalculate left =
new ForkJoinCalculate(func, start, mid, step);
ForkJoinCalculate right =
new ForkJoinCalculate(func, mid, end, step);
left.fork();
double rightAns = right.compute();
double leftAns = left.join();
return leftAns + rightAns;
}
}
Collector
53. The F/J framework Criticism
• exceedingly complex
– The code looks more like an old C language program that was
segmented into classes than an O-O structure
• a design failure
– It’s primary uses are for fully-strict, compute-only, recursively
decomposing processing of large aggregate data structures. It is for
compute intensive tasks only
• lacking in industry professional attributes
– no monitoring, no alerting or logging, no availability for general
application usage
• misusing parallelization
– recursive decomposition has narrower performance window. An
academic exercise
• inadequate in scope
– you must be able to express things in terms of apply, reduce, filter,
map, cumulate, sort, uniquify, paired mappings, and so on — no
general purpose application programming here
• special purpose
55. F/J restrictions
• Recursive decomposition has narrower performance window. It
only works well:
– on balanced tree structures (DAG),
– where there are no cyclic dependencies,
– where the computation duration is neither too short nor too long,
– where there is no blocking
• Recommended restrictions:
– must be plain (between 100 and 10,000 basic computational steps in
the compute method),
– compute intensive code only,
– no blocking,
– no I/O,
– no synchronization F/J
All problems
58. 1994
“He (Bill Joy) would often go on at length about
how great Oak would be if he could only add
closures and continuations and parameterized
types”
Patrick Naughton,
one of the creators of the Java
59. 1994
“He (Bill Joy) would often go on at length about
how great Oak would be if he could only add
closures and continuations and parameterized
types”
“While we all agreed these were very cool language
features, we were all kind of hoping to finish this
language in our lifetimes and get on to creating cool
applications with it”
Patrick Naughton,
one of the creators of the Java
60. 1994
“He (Bill Joy) would often go on at length about
how great Oak would be if he could only add
closures and continuations and parameterized
types”
“While we all agreed these were very cool language
features, we were all kind of hoping to finish this
language in our lifetimes and get on to creating cool
applications with it”
“It is also interesting that Bill was absolutely right
about what Java needs long term. When I go look
at the list of things he wanted to add back then, I
want them all. He was right, he usually is”
Patrick Naughton,
one of the creators of the Java
61. Ingredients of lambda expression
• A lambda expression has three ingredients:
– A block of code
– Parameters
– Values for the free variables; that is, the variables that
are not parameters and not defined inside the code
62. Ingredients of lambda expression
• A lambda expression has three ingredients:
– A block of code
– Parameters
– Values for the free variables; that is, the variables that
are not parameters and not defined inside the code
while (x < end) {
result += step * (sin(x) * sin(x) + cos(x) * cos(x));
x += step;
}
63. Ingredients of lambda expression
• A lambda expression has three ingredients:
– A block of code
– Parameters
– Values for the free variables; that is, the variables that
are not parameters and not defined inside the code
while (x < end) {
result += step * (sin(x) * sin(x) + cos(x) * cos(x));
x += step;
}
step * (sin(x) * sin(x) + cos(x) * cos(x));
64. Ingredients of lambda expression
• A lambda expression has three ingredients:
– A block of code
– Parameters
– Values for the free variables; that is, the variables that
are not parameters and not defined inside the code
while (x < end) {
result += step * (sin(x) * sin(x) + cos(x) * cos(x));
x += step;
}
step * (sin(x) * sin(x) + cos(x) * cos(x));
A block of code
65. Ingredients of lambda expression
• A lambda expression has three ingredients:
– A block of code
– Parameters
– Values for the free variables; that is, the variables that
are not parameters and not defined inside the code
while (x < end) {
result += step * (sin(x) * sin(x) + cos(x) * cos(x));
x += step;
}
step * (sin(x) * sin(x) + cos(x) * cos(x));
A block of code
Parameter(s)
66. Ingredients of lambda expression
• A lambda expression has three ingredients:
– A block of code
– Parameters
– Values for the free variables; that is, the variables that
are not parameters and not defined inside the code
while (x < end) {
result += step * (sin(x) * sin(x) + cos(x) * cos(x));
x += step;
}
step * (sin(x) * sin(x) + cos(x) * cos(x));
A block of code
Parameter(s)Free variable