Ну вот она самая интересная тема по моему мнению)
Про процессы, потоки
Во время танца мы управляем не только руками, ногами и головой, но и делаем множество интересных и точных движений, которые могут быть выполнены только продвинутыми танцорами с высоким уровнем развития мозга. Каждое движение можно рассматривать как процесс, а компьютеры, как известно, бывают четырехъядерные, восьмиядерные и т.д.
Обычно Количество возможных процессов зависит от количества ядер в компьютере, и аналогично, от уровня мозговой активности танцора зависит насколько будет красиво исполнение сложных движений. То есть восьми ядерный процессор способен выполнить параллельно только 8 процессов.
Таким образом, танцоры-профессионалы можно сравнить с многоядерными компьютерами, в то время как мы, обычные люди, являемся одноядерными, делая простые движения руками или ногами.
Процесс — окружение или среда выполнения для запуска программы(набора файлов) на ОС. Создав, предоставляет доступ к системным ресурсам: память, устройства ввода\вывода, камера и тд.
Потоки — это единицы выполнения (unit of execution), которые выполняются на процессоре. В одном процессе может находится несколько потоков. Такие потоки будут иметь общую память.
Когда мы открываем flutter-приложение(или dart-код) оно запускается в одном процессе и в главном потоке.
На картинке мы видим что каждому процессу выделяется память, к которой имеют доступ все потоки этого процесса.
Асинхронность и параллельность
И так, ты знаешь async, await, Future и думаешь что победил время.
Попробуй асинхронно запустить код, которое занимает достаточное время и ты увидишь, как твоя анимация затормозит, а само приложение перестанет отвечать на действия пользователя. Основной изолят будет перегружен.
Вот например из официальной документации:
const String filename = 'with_keys.json';
void main() async {
// Read some data.
final fileData = await _readFileAsync();
// ожидание результата сложной функции
// приложение остановливается
final jsonData = jsonDecode(fileData);
// Use that data.
print('Number of JSON keys: ${jsonData.length}');
}
Future<String> _readFileAsync() async {
final file = File(filename);
final contents = await file.readAsString();
return contents.trim();
}
Или даже какая-то такая функция:
int value = 0;
for (var i = 0; i < 5000000; i++) {
value += i;
print(value);
}
Попробуйте ее запустить в DartPad.
Асинхронность это хорошо, но для таких задач как поход в сеть или получение данных, где основную работу выполняет сервер.
А вот параллельность поможет нам в приложения производить затратные операции без потери производительности и времени.
Асинхронность работает в одном потоке, параллельность в двух и более.
Мы привыкли путать параллельность, многопоточность, асинхронность.
Ты можешь почитать эти статьи , если тебе интересна эта тема :
Параллелизм против многопоточности против асинхронного программирования: разъяснение
Concurrency and Parallelism in Dart and how it is used in Flutter
Превращаем приложение в super-Танцора. Isolates
И так если загрузить майн EventLoop (главный изолят) сложной задачей вы получите блокировку потока, на то время пока не выполнится эта задача. Для реализации параллельного выполнения кода в Dart есть Isolate:
У каждого isolate есть свой поток, память и следовательно и EventLoop.
Гарантируется что в одном потоке выполняется одновременно только один изолят.
Поток не привязан к конкретному изоляту.
Общение между изолятами достигается с помощью сообщений, которые передаются через порты
SendPort
иReceivePort
.
Процесс содержит в себе потоки, которые имеют общую память. А Изоляты не имеют общей памяти именно поэтому Isolate != поток.
Преимущества
так как изолят имеет свою память и общается по портам, похожими механизмами могут похвастаться совсем немногие языки. Такие языки как JAva,Kotlin, C++ используют спокойно используют thread и корутины, однако подход в dart имеет свои плюсы:
Параллелизм с общим состоянием подвержен ошибкам и может привести к усложнению кода.
Отсутствие общего состояния между изолятами означает, что сложности параллелизма, такие как мьютексы или блокировки и перегонки данных, не будут возникать в Dart.
Недостатки
Веб-платформа Dart, однако, не поддерживает изоляты. Веб-приложения Dart могут использовать web workers для запуска сценариев в фоновых потоках, аналогичных изолятам.
Невозможность работать с UI. Сторонний поток не может получить доступ к памяти и к всему что происходит внутри главного main thread, в котором как раз таки и находится UI.
В изолят может быть передана только глобальная функция или статический методы, как просто набор простых операций для выполнения. Так как методы или объекты существуют в своем контексте и мы не можем его предать в другой изолят.
Время на передачу сообщений между изолятами. Если в многопоточности мы просто передавали ссылки на объекты и это не занимало много времени, то в dart идет копирование данных, которое занимает достаточно много времени.
Простой способ создания Isolate
Будем использовать compute()
или Isolate.run()
Порождает (запускает и создает) изолят
Запускает функцию для созданного изолята
Фиксирует результат
Возвращает результат в основной изолят
Завершает изолят после завершения работы
Проверяет, фиксирует и отправляет исключения и ошибки обратно в основной изолят
Передача одного сообщения от Изолята в Главный
Создаем порт для главного изолята, как бы трубу которая будет предназначена для получения сообщений.
Передаем функцию и параметры нужные для нее, в данном примере SendPort.
SendPort
по сути, просто указывает на свойReceivePort
, а также сохраняет идентификатор того,Isolate
где он был создан. Любое сообщение, отправленное через этотSendPort
, доставляется на этотReceivePort
, из которого оно было создан.У
ReceivePort
(порта приема) может быть многоSendPort
(портов отправки).
import 'dart:isolate';
void main() async{
final receivePort = ReceivePort();
// создаем порт для главного изолята,
как бы трубу которая будет предназанчена для получения сообщений.
final isolate = await Isolate.spawn(count, receivePort.sendPort);
receivePort.listen((message) {
print(message);
receivePort.close();
isolate.kill();
});
print("Some work...");
}
void count(SendPort sendPort) {
var result = 0;
for (var i = 1; i <= 50000000000; i++) {
result = i;
}
sendPort.send(result);
}
Двухстороннее общение
Пример из статьи, мне он показался интересным, я его немного изменила.
import 'dart:async';
import 'dart:isolate';
void main() => Future<void>(() async {
final mainIsolatePort = ReceivePort();
// создаем порт получения для главного изолята
final isolate = await Isolate.spawn<SendPort>(
entryPoint,
mainIsolatePort.sendPort,
errorsAreFatal: true,
debugName: 'MyIsolate',
);
// создаем новый изолят, указываем входную функцию,
// передаем порт отправки для главного изолята.
// errorsAreFatal: true гарантирует, что неперехваченные ошибку приведут
// к уничтожению нового изолята.
// debugName: 'MyIsolate' дает имя изоляту.
final completer = Completer<SendPort>();
// для чего комплитор расскажу ниже
//(ожидаем когда из нового изолята придет сообщние с его портом отправки)
mainIsolatePort.listen((message) {
if (message is SendPort) completer.complete(message);
if (message is String) print(message);
});
final send2Isolate = await completer.future;
// Получаем SendPort.
send2Isolate.send(7);
send2Isolate.send('kjb');
send2Isolate.send(9);
// Отправляем сообщения новому изоляту.
await Future<void>.delayed(const Duration(seconds: 1));
mainIsolatePort.close(); // Close the ReceivePort.
isolate.kill(); // Kill the isolate.
});
void entryPoint(SendPort mainIsolatePortSend) {
final isolatePort = ReceivePort();
// Создаем ReceivePort у нового изолята,
// для того что бы он мог получать сообщение от главного.
mainIsolatePortSend.send(isolatePort.sendPort);
// Send the SendPort to the main isolate.
isolatePort.listen((message) {
if (message is! int) return; // Ignore messages of other types.
for (var i = 1, r = 1; i <= message; i++, r *= i) {
// отправляем результат главному изоляту через порт получения главного изолята,
// который мы передади в эту входную функцию.
mainIsolatePortSend.send('$i! = $r');
}
}
// слушаем порт получения, если сообщение не число, мы игнорируем его.
);
}
Мы создаем для каждого изолята ReceivePort
, SendPort.
Я бы представила это как-то так:
Помним о том, чтоReceivePort
могут иметь несколько SendPort
, поэтому картинка могла бы выглядеть так, если бы новый изолят общался бы с другими изолятами:
Комплитор здесь создан для того что бы дождаться сообщения от нового изолята sendPort
и затем использовать его.
Как ты видишь изолят ни как не обработал строку, отправленную главным изолятом.
send2Isolate.send(7);
send2Isolate.send('kjb');
send2Isolate.send(9);
На это знакомство с изолятами закончено :)
Рекомендую почитать:
О Isolate.spawnUri
узнаешь тут: https://dart.dev/language/concurrency
О улучшении кода при работе с изолятами тут: https://plugfox.dev/mastering-isolates/
Как тебе статья?)