Ну вот она самая интересная тема по моему мнению)

Про процессы, потоки

Во время танца мы управляем не только руками, ногами и головой, но и делаем множество интересных и точных движений, которые могут быть выполнены только продвинутыми танцорами с высоким уровнем развития мозга. Каждое движение можно рассматривать как процесс, а компьютеры, как известно, бывают четырехъядерные, восьмиядерные и т.д.

Обычно Количество возможных процессов зависит от количества ядер в компьютере, и аналогично, от уровня мозговой активности танцора зависит насколько будет красиво исполнение сложных движений. То есть восьми ядерный процессор способен выполнить параллельно только 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.

Асинхронность это хорошо, но для таких задач как поход в сеть или получение данных, где основную работу выполняет сервер.

А вот параллельность поможет нам в приложения производить затратные операции без потери производительности и времени.

Асинхронность работает в одном потоке, параллельность в двух и более.

Мы привыкли путать параллельность, многопоточность, асинхронность.

Ты можешь почитать эти статьи , если тебе интересна эта тема :

  1. Параллелизм против многопоточности против асинхронного программирования: разъяснение

  2. Concurrency and Parallelism in Dart and how it is used in Flutter

  3. Многопоточность против параллелизма, чем они отличаются?

  4. The Difference Between Asynchronous and Multi-Threading

  5. Официальное

  6. Короткое и понятное

Превращаем приложение в super-Танцора. Isolates

И так если загрузить майн EventLoop (главный изолят) сложной задачей вы получите блокировку потока, на то время пока не выполнится эта задача. Для реализации параллельного выполнения кода в Dart есть Isolate:

  • У каждого isolate есть свой поток, память и следовательно и EventLoop.

  • Гарантируется что в одном потоке выполняется одновременно только один изолят.

  • Поток не привязан к конкретному изоляту.

  • Общение между изолятами достигается с помощью сообщений, которые передаются через портыSendPort и ReceivePort .

Процесс содержит в себе потоки, которые имеют общую память. А Изоляты не имеют общей памяти именно поэтому Isolate != поток.

Преимущества

так как изолят имеет свою память и общается по портам, похожими механизмами могут похвастаться совсем немногие языки. Такие языки как JAva,Kotlin, C++ используют спокойно используют thread и корутины, однако подход в dart имеет свои плюсы:

Недостатки

  • Веб-платформа Dart, однако, не поддерживает изоляты. Веб-приложения Dart могут использовать web workers для запуска сценариев в фоновых потоках, аналогичных изолятам. 

  • Невозможность работать с UI. Сторонний поток не может получить доступ к памяти и к всему что происходит внутри главного main thread, в котором как раз таки и находится UI.

  • В изолят может быть передана только глобальная функция или статический методы, как просто набор простых операций для выполнения. Так как методы или объекты существуют в своем контексте и мы не можем его предать в другой изолят.

  • Время на передачу сообщений между изолятами. Если в многопоточности мы просто передавали ссылки на объекты и это не занимало много времени, то  в dart идет копирование данных, которое занимает достаточно много времени.

Простой способ создания Isolate

Будем использовать compute() или Isolate.run()

В этих примерах реализован основной изолят, который порождает простой рабочий изолят. Isolate.run() упрощаются шаги по настройке рабочих изолятов и управлению ими:
  1. Порождает (запускает и создает) изолят

  2. Запускает функцию для созданного изолята

  3. Фиксирует результат

  4. Возвращает результат в основной изолят

  5. Завершает изолят после завершения работы

  6. Проверяет, фиксирует и отправляет исключения и ошибки обратно в основной изолят

Передача одного сообщения от Изолята в Главный

Создаем порт для главного изолята, как бы трубу которая будет предназначена для получения сообщений.

Передаем функцию и параметры нужные для нее, в данном примере 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');
    }
  }
// слушаем порт получения, если сообщение не число, мы игнорируем его.
);
}

Мы создаем для каждого изолята ReceivePortSendPort.Я бы представила это как-то так:

Помним о том, чтоReceivePortмогут иметь несколько SendPort, поэтому картинка могла бы выглядеть так, если бы новый изолят общался бы с другими изолятами:

Комплитор здесь создан для того что бы дождаться сообщения от нового изолята sendPort и затем использовать его.

Консоль
Консоль

Как ты видишь изолят ни как не обработал строку, отправленную главным изолятом.

send2Isolate.send(7);
send2Isolate.send('kjb');
send2Isolate.send(9); 

На это знакомство с изолятами закончено :)

Рекомендую почитать:

О Isolate.spawnUri узнаешь тут: https://dart.dev/language/concurrency

О улучшении кода при работе с изолятами тут: https://plugfox.dev/mastering-isolates/

Как тебе статья?)

Комментарии (0)