142
Конкурентное (многопоточное)
выполнение
пользованию экземпляры
ThreadPoolExecutor
). Использование пула потоков
позволяет
контролировать объем работы, выполняемой в фоновом режиме,
а также ставить задачи в очередь и удаляет их по мере выполнения. Загляните
в документацию с описанием
ThreadPoolExecutor
(
https://oreil.ly/9pV5G
) и
Execu
tors
(
https://oreil.ly/
WKIRY
).
Возможно, вы слышали, что конкурентное выполнение – это один из самых
сложных аспектов в информатике, и мы не будем притворяться, что в нем нет
ничего чрезвычайно сложного, но большинство из этих сложностей спрятаны
от нас за фасадом системы или легко обходятся стороной. Как-то Майк (один
из авторов книги) сказал, что «многопоточность реализуется просто; сложнее
реализовать синхронизацию». То есть, как было показано выше, мы легко мо
-
жем запустить некоторую задачу в фоновом потоке, но за этим утверждением
следует 3500 звездочек (извините за гиперболу). Например, в Android нельзя
получить доступ ни к какому экземпляру
View
, кроме как из основного потока.
Это действительно создает большие сложности! В методе
Runnable.run
нельзя
даже
ссылаться
на экземпляр
View
без риска получить
RuntimeException
. Кроме
того, порядок завершения фоновых потоков никак не гарантируется; и здесь
мы начинаем вдаваться в сложности конкурентного выполнения – байт-код,
который генерирует компилятор Java, не всегда выглядит так же, как исходный
код на Java.
Вот классический пример «небезопасного» потока выполнения:
Java
public class MainAc ti vi ty extends Ac ti vi ty {
private static final int MAX = 1000;
private int mCounter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); // имеется
представление с кнопкой
findViewById(R.id.button).setOnClickListener(view > {
while (mCounter < MAX) {
new Thread(() > {
Log.d("MyTag", String.valueOf(mCounter++));
}).start();
}
});
}
}
Kotlin
class MainAc ti vi ty : Ac ti vi ty() {
private const val MAX = 1000
private var counter: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) // имеется представление с кнопкой
Android
143
findViewById(R.id.button).setOnClickListener {
while (counter < MAX) {
Thread { Log.d("MyTag", counter++.toString()) }.start()
}
}
}
}
В зависимости от особенностей устройства и окружения может потребовать
-
ся запустить больше или меньше параллельных задач, чтобы увидеть эффект,
но, поэкспериментировав с этой простой задачей, вы рано или поздно увиди
-
те, что окончательное значение в журнале
не равно
9999. Но не будем тратить
ваше и наше время на объяснение нюансов безопасности многопоточного вы
-
полнения и сразу скажем, что байт-код оператора инкремента выглядит при
-
мерно так:
1) получить значение
mCounter
из памяти;
2) прибавить 1
к значению
mCounter
;
3) записать результат обратно в память.
Поскольку система быстро-быстро переключает параллельные потоки, что
-
бы сымитировать параллельное выполнение операций (это называется «пере
-
ключением контекста»), поток № 391 может прочитать значение
mCounter
в тот
момент, когда поток № 390 уже выполнил шаг 1, но еще не выполнил шаг 2. Эта
конкретная ситуация называется состоянием гонки. Данная проблема широко
известна как взаимовлияние потоков и является одним из примеров недоста
-
точной безопасности при многопоточном выполнении. Это далеко неполное
исследование рисков конкурентного программирования – некоторые реали
-
зации Java позволяют потокам создавать копии переменных, чтобы избежать
дорогостоящих вызовов IPC (Interprocess Communication – межпроцессные
взаимодействия), поэтому, когда несколько потоков изменяют один и тот же
экземпляр объекта, его состояние не может быть гарантировано.
Для борьбы с этим эффектом существуют специальные механизмы, та
-
кие
как ключевые слова
synchronized
и
volatile
и классы
Atomic
. И поскольку
структуры данных особенно уязвимы (представьте, что один поток выполняет
обход содержимого структуры данных, а другой добавляет или удаляет эле
-
менты к ней), некоторые структуры имеют потокобезопасные (и менее произ
-
водительные) версии или вспомогательные методы, помогающие уменьшить
риски.
Более глубокое обсуждение проблем конкурентного выполнения выходит за
рамки этой главы. Могут потребоваться годы, чтобы освоить многопоточное
программирование даже в одном технологическом стеке.
Некоторые разра
-
ботчики никогда по-настоящему не понимают, что происходит за кулисами,
и это нормально! Потрясающий разработчик пользовательского интерфейса
может использовать совершенно иные приемы конкурентного программиро
-
вания, чем программист, работающий с большими данными.
Один простой
трюк в Android заключается в том, чтобы запустить вычисления в фоновом
потоке, а затем вызвать
Ac ti vi ty.runOnUiThread
,
View.post
или
Handler.post
, дабы
передать результаты в основной поток, чтобы гарантировать последователь
-
ное выполнение и невосприимчивость к действиям других процессов.