[Перевод] Асинхронное программирование на Flutter: будущее, изоляция и цикл событий

Программа перевода самородков Flutter

В этой статье представлены различные режимы выполнения кода во Flutter: однопоточный, многопоточный, синхронный и асинхронный.

Сложность:средний

резюме

Я недавно получил некоторые сFuture,async,await,IsolateА также некоторые проблемы, связанные с концепцией параллельного исполнения.

Из-за этих проблем у некоторых людей возникают проблемы с выполнением кода.

Я думаю, это объясняется статьейасинхронный,параллельноОчень полезно работать над этими понятиями и устранять неоднозначность любого из них.


Dart — однопоточный язык

Прежде всего, нужно иметь в виду,Dartдаодин потока такжеFlutterзависит отDart.

фокус

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

То есть, если вы считаетечистая синхронизацияметод, то пока он не завершится, последний будетТолькоМетод, который необходимо выполнить.

void myBigLoop(){
    for (int i = 0; i < 1000000; i++){
        _doSomethingSynchronously();
    }
}

В приведенном выше примереmyBigLoop()Метод никогда не прерывается, пока его выполнение не будет завершено. Поэтому, если метод займет некоторое время, приложение будетблокировать.


Dartмодель исполнения

Итак, за кулисамиDartКак вы управляете выполнением последовательности операций?

Чтобы ответить на этот вопрос, нам нужно рассмотретьDartСеквенсор кода (цикл событий).

когда вы начинаетеFlutter(или любойDart) При применении создаст и начну новыйнитьпроцесс (вDartв "Isolate»). Долженнитьбудет единственной вещью, на которой вам нужно сосредоточиться во всем приложении.

Итак, после создания этого потока Dart автоматически:

  1. Инициализировать 2 очереди FIFO (первым пришел, первым обслужен) ("MicroTask"а также "Event");
  2. И когда метод завершит выполнение, выполнитеmain()метод,
  3. запускатьцикл событий.

В течение всего времени жизни потока один вызываетсяцикл событийизОдинА скрытый процесс будет определять, как и в каком порядке будет выполняться ваш код (в зависимости отMicroTaskа такжеEventочередь).

цикл событийэтонеограниченныйцикл (управляемый внутренними часами) на каждомв течение тактового цикла,Если никакой другой код Dart не выполняется, затем выполните следующие действия:

void eventLoop(){
    while (microTaskQueue.isNotEmpty){
        fetchFirstMicroTaskFromQueue();
        executeThisMicroTask();
        return;
    }

    if (eventQueue.isNotEmpty){
        fetchFirstEventFromQueue();
        executeThisEventRelatedCode();
    }
}

Как мы можем видеть,MicroTaskОчередь имеет приоритет надEventОчередь, какова роль этих двух очередей?

Очередь микрозадач

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

так какMicroTaskВ качестве примера можно представить, что ресурс должен быть освобожден, как только он будет закрыт. Поскольку процесс выключения может занять некоторое время, вы можете написать свой код следующим образом:

MyResource myResource;

...

void closeAndRelease() {
    scheduleMicroTask(_dispose);
    _close();
}

void _close(){
    // 代码以同步的方式运行
    // 以关闭资源
    ...
}

void _dispose(){
    // 代码在
    // _close() 方法
    // 完成后执行
}

Это то, что вам не нужно использовать большую часть времени. Например, на протяженииFlutterМетод scheduleMicroTask() упоминается в исходном коде всего 7 раз.

Лучше расставить приоритеты, используяEventочередь.

Очередь событий

EventОчереди доступны для следующих эталонных моделей

  • внешние события, такие как
    • ввод/вывод;
    • жест;
    • рисунок;
    • таймер;
    • поток;
    • ...
  • futures

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

раз нетmicro taskбегать,цикл событийрассмотрюEventпервый элемент в очереди и выполнить его.

Примечательно,FutureОперация также черезEventОбработка очереди.


Future

FutureЯвляетсяасинхронныйкоторый выполняется и завершается (или терпит неудачу) в какой-то момент в будущемЗадача.

когда вы создаете экземплярFutureВремя:

  • ДолженFutureЭкземпляр создается и записываетсяDartВ управляемом внутреннем массиве;
  • нужно этоFutureИсполняемый код помещается непосредственно вEventидти в очередь
  • Долженбудущий экземплярвернуть статус (= незавершенный);
  • Если есть следующий синхронный код, выполнить его (Выполнение кода, который не является Future)

если толькоцикл событийотEventпопасть в петлю, бытьFutureСсылочный код будет похож на любой другойEventДелать то же самое.

Когда этот код будет выполнен и завершится (или завершится с ошибкой),then()илиcatchError()Запускается прямой метод.

Чтобы проиллюстрировать это, давайте рассмотрим следующий пример:

void main(){
    print('Before the Future');
    Future((){
        print('Running the Future');
    }).then((_){
        print('Future is complete');
    });
    print('After the Future');
}

Если мы запустим этот код, вывод будет выглядеть так:

Before the Future
After the Future
Running the Future
Future is complete

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

  1. print('Перед будущим')
  2. Буду(){print('Управляя будущим');}Добавить в очередь событий;
  3. print('После будущего')
  4. цикл событийПолучите код (указанный на втором шаге) и выполните его.
  5. Когда код выполняется, он ищетthen()оператор и выполнить его

Некоторые очень важные вещи, которые нужно помнить:

Future нетвыполняться параллельно, но следоватьцикл событийПоследовательное выполнение правил обработки событий.


Асинхронный метод

когда вы используетеasyncключевое слово в качестве суффикса к объявлению метода,Dartбудет интерпретироваться как:

  • Возвращаемое значение представляет собойFuture;
  • ЭтоСинхронизироватьКод, выполняющий метод доПервое ключевое слово ожидания, то он приостанавливает выполнение остальной части метода;
  • Один разawaitссылка на ключевое словоFutureКогда выполнение будет завершено, следующая строка кода будет выполнена немедленно.

Понимание этогоОчень важный, потому что многие разработчики думаютawaitприостановил весь процессдоон выполняется до конца, но фактэто неправда. они забылицикл событийрежим работы...

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

void main() async {
  methodA();
  await methodB();
  await methodC('main');
  methodD();
}

methodA(){
  print('A');
}

methodB() async {
  print('B start');
  await methodC('B');
  print('B end');
}

methodC(String from) async {
  print('C start from $from');

  Future((){                // <== 该代码将在未来的某个时间段执行
    print('C running Future from $from');
  }).then((_){
    print('C end of Future from $from');
  });

  print('C end from $from');
}

methodD(){
  print('D');
}

Правильный порядок:

  1. A
  2. B start
  3. C start from B
  4. C end from B
  5. B end
  6. C start from main
  7. C end from main
  8. D
  9. C running Future from B
  10. C end of Future from B
  11. C running Future from main
  12. C end of Future from main

Теперь давайте рассмотрим, что в приведенном выше кодеmethodC()Для вызовов на сервер это может занять неравномерное время для ответа. Я считаю очевидным, что предсказать точный поток выполнения может стать очень сложно.

Если вы изначально хотели, чтобы код примера выполнялся только в конце всего кодаmethodD(), то вы должны написать свой код следующим образом:

void main() async {
  methodA();
  await methodB();
  await methodC('main');
  methodD();
}

methodA(){
  print('A');
}

methodB() async {
  print('B start');
  await methodC('B');
  print('B end');
}

methodC(String from) async {
  print('C start from $from');

  await Future((){                  // <== 在此处进行修改
    print('C running Future from $from');
  }).then((_){
    print('C end of Future from $from');
  });
  print('C end from $from');
}

methodD(){
  print('D');
}

Выходная последовательность:

  1. A
  2. B start
  3. C start from B
  4. C running Future from B
  5. C end of Future from B
  6. C end from B
  7. B end
  8. C start from main
  9. C running Future from main
  10. C end of Future from main
  11. C end from main
  12. D

Факт черезmethodC()определено вFutureгде просто добавитьawaitизменит все поведение.

Кроме того, имейте в виду:

async нетвыполнять параллельно, также следоватьцикл событийПоследовательное выполнение правил обработки событий.

Последний пример, который я хочу вам показать, выглядит следующим образом. бегатьmethod1а такжеmethod2Каков результат? Будут ли они одинаковыми?

void method1(){
  List<String> myArray = <String>['a','b','c'];
  print('before loop');
  myArray.forEach((String value) async {
    await delayedPrint(value);
  });
  print('end of loop');
}

void method2() async {
  List<String> myArray = <String>['a','b','c'];
  print('before loop');
  for(int i=0; i<myArray.length; i++) {
    await delayedPrint(myArray[i]);
  }
  print('end of loop');
}

Future<void> delayedPrint(String value) async {
  await Future.delayed(Duration(seconds: 1));
  print('delayedPrint: $value');
}

Отвечать:

method1() method2()
1. before loop 1. before loop
2. end of loop 2. delayedPrint: a (after 1 second)
3. delayedPrint: a (after 1 second) 3. delayedPrint: b (1 second later)
4. delayedPrint: b (directly after) 4. delayedPrint: c (1 second later)
5. delayedPrint: c (directly after) 5. end of loop (right after)

Знаете ли вы разницу и почему они ведут себя по-разному?

Ответ основан на том, что,method1использоватьforEach()функция для перебора массива. На каждой итерации вызываетсяasync(отсюдаFuture) новая функция обратного вызова. Выполняйте обратный вызов до тех пор, пока он не встретитawait, а затем отправьте остальную часть кода вEventочередь. После завершения итерации выполняется следующий оператор: «print('конец цикла')». После завершения выполненияцикл событийЗарегистрированные 3 обратных вызова будут обработаны.

дляmethod2Весь контент запускается в одном и том же кодовом «блоке», поэтому в этом примере его можно выполнять по порядку.

Как видите, даже в, казалось бы, очень простом коде нам все равно нужно иметь в видуцикл событийКак это работает...


Многопоточность

Итак, как мы запускаем код параллельно во Flutter? Является ли это возможным?

да, благодаряIsolates.


Что такое Изолировать?

Как объяснялось ранее,IsolateдаDartсерединанить.

Однако он отличается от обычного"нитьЕсть большая разница в реализации ", которая также называется "Isolate"причина.

«Изолировать» во Flutterне разделяет память. Между различными «Изолировать» через «Информация" общаться.


У каждого изолята есть свойцикл событий

каждый "Isolate"есть свои"цикл событий” и очереди (MicroTask и Event). Это означает, что вIsolateКод, который запускается, и другойIsolateТам нет ассоциации.

Благодаря этому мы можем получитьпараллельная обработкаСпособность.


Как начать изолировать?

бежать согласно тебеIsolateсценариях вам может потребоваться рассмотреть другой подход.

1. Базовые решения

Первое решение не зависит ни от какого пакета, оно полностью зависит отDartПредоставляется низкоуровневый API.

1.1. Шаг 1: Создание и рукопожатие

Как упоминалось ранее,Isolateне разделяет никакой памяти и взаимодействует через сообщения, поэтому нам нужно найти способ общения между «звонящим» и новымisolateустановить связь между ними.

каждыйIsolateОба выставляют сообщение, переходящее кIsolateназывается "SendPort"изпорт. (Лично, название немного вводит в заблуждение, потому что этополучать / слушать, но ведь это официальное название).

это означает"абонент"а также"новый изолят«Нужно знать порты друг друга, чтобы общаться. Процесс этого рукопожатия выглядит следующим образом:

//
// 新的 isolate 端口
// 该端口将在未来使用
// 用来给 isolate 发送消息
//
SendPort newIsolateSendPort;

//
// 新 Isolate 实例
//
Isolate newIsolate;

//
// 启动一个新的 isolate
// 然后开始第一次握手
//
//
void callerCreateIsolate() async {
    //
    // 本地临时 ReceivePort
    // 用于检索新的 isolate 的 SendPort
    //
    ReceivePort receivePort = ReceivePort();

    //
    // 初始化新的 isolate
    //
    newIsolate = await Isolate.spawn(
        callbackFunction,
        receivePort.sendPort,
    );

    //
    // 检索要用于进一步通信的端口
    //
    //
    newIsolateSendPort = await receivePort.first;
}

//
// 新 isolate 的入口
//
static void callbackFunction(SendPort callerSendPort){
    //
    // 一个 SendPort 实例,用来接收来自调用者的消息
    //
    //
    ReceivePort newIsolateReceivePort = ReceivePort();

    //
    // 向调用者提供此 isolate 的 SendPort 引用
    //
    callerSendPort.send(newIsolateReceivePort.sendPort);

    //
    // 进一步流程
    //
}

ограничениеизолировать"Вход"долженявляется функцией верхнего уровня илистатическийметод.

1.2. Шаг 2. Отправьте сообщение в Изолировать

Теперь, когда у нас есть порт для отправки сообщений в Isolate, давайте посмотрим, как это сделать:

//
// 向新 isolate 发送消息并接收回复的方法
//
//
// 在该例中,我将使用字符串进行通信操作
// (发送和接收的数据)
//
Future<String> sendReceive(String messageToBeSent) async {
    //
    // 创建一个临时端口来接收回复
    //
    ReceivePort port = ReceivePort();

    //
    // 发送消息到 Isolate,并且
    // 通知该 isolate 哪个端口是用来提供
    // 回复的
    //
    newIsolateSendPort.send(
        CrossIsolatesMessage<String>(
            sender: port.sendPort,
            message: messageToBeSent,
        )
    );

    //
    // 等待回复并返回
    //
    return port.first;
}

//
// 扩展回调函数来处理接输入报文
//
static void callbackFunction(SendPort callerSendPort){
    //
    // 初始化一个 SendPort 来接收来自调用者的消息
    //
    //
    ReceivePort newIsolateReceivePort = ReceivePort();

    //
    // 向调用者提供该 isolate 的 SendPort 引用
    //
    callerSendPort.send(newIsolateReceivePort.sendPort);

    //
    // 监听输入报文、处理并提供回复的
    // Isolate 主程序
    //
    newIsolateReceivePort.listen((dynamic message){
        CrossIsolatesMessage incomingMessage = message as CrossIsolatesMessage;

        //
        // 处理消息
        //
        String newMessage = "complemented string " + incomingMessage.message;

        //
        // 发送处理的结果
        //
        incomingMessage.sender.send(newMessage);
    });
}

//
// 帮助类
//
class CrossIsolatesMessage<T> {
    final SendPort sender;
    final T message;

    CrossIsolatesMessage({
        @required this.sender,
        this.message,
    });
}
1.3. Шаг 3. Уничтожьте новый экземпляр Isolate.

Когда вам больше не нужен этот новый экземпляр Isolate, лучше всего выпустить его:

//
// 释放一个 isolate 的例程
//
void dispose(){
    newIsolate?.kill(priority: Isolate.immediate);
    newIsolate = null;
}
1.4. Специальные примечания — потоковая передача одного слушателя

Вы могли заметить, что мы используемпотоксуществует"абонент' и новыйisolateобщение между ними. ЭтипотокТип: "единственный слушатель"поток.


2. Разовый расчет

Если вам просто нужно запустить какой-то код для выполнения определенной работы, и после того, как работа будет выполнена, вам не нужноIsolateвзаимодействовать, то есть очень удобныйcomputeизHelper.

В основном включают следующие функции:

  • сформировал одинIsolate,
  • управлятьПерезвонитеи передать некоторые данные,
  • Возвращает результат обработки функции обратного вызова,
  • Завершить после выполнения обратного вызоваIsolate.

ограничение

"Перезвонитедолженявляется функцией верхнего уровня ине можемявляется замыканием или методом в классе (статическом или нестатическом).


3. Важные ограничения

Во время написания этой статьи было важно обнаружить это

Связь между платформой и каналомТолькоЗависит отосновная изолирующая поддержка. Долженосновной изолятСоответствует тому, который создается при запуске приложенияisolate.

То есть программно созданныйisolateпример, не может быть реализованPlatform-Channelкоммуникация...

Тем не менее, есть обходной путь... Пожалуйста, обратитесь кэта связьдля обсуждения этой темы.


Когда мне следует использовать Futures и Isolate?

Пользователи будут оценивать качество приложения по разным факторам, таким как:

  • характеристика
  • Внешний вид
  • Удобство для пользователя
  • ...

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

Итак, вот некоторые моменты, которые вы должны учитывать в процессе разработки системы:

  1. Если фрагмент кодане можемпрервано, используйтеТрадицияпроцесс синхронизации (один или несколько методов, которые вызывают друг друга);
  2. Если фрагмент кода может работать независимоНетВлияют на производительность приложения, вы можете рассмотретьFutureиспользоватьцикл событий;
  3. Если тяжелая обработка может занять некоторое время и может повлиять на производительность вашего приложения, рассмотрите возможность использованияIsolate.

Другими словами, рекомендуется использовать как можно большеFuture(прямо или косвенно черезasyncметод), потому что когда-тоцикл событийесть свободное время, этиFutureкод будет выполнен. Это позволит пользователямЧувствоватьВещи обрабатываются параллельно (и теперь мы знаем, что это не так).

Другой может помочь вам принять решение об использованииFutureилиIsolateФактор — это среднее время, необходимое для запуска некоторого кода.

  • Если метод требует несколькихмиллисекунда => Future
  • Если поток обработки требует сотенмиллисекунда => Isolate

Вот несколько хорошихIsolateОпции:

  • JSONДекодирование: декодирование JSON (ответ HttpRequest) может занять некоторое время => использоватьcompute
  • Шифрование: Шифрование может занять много времени =>Isolate
  • Обработка изображений: обработка изображений (например, обрезка) занимает некоторое время =>Isolate
  • Загрузка изображений из Интернета: в этом случае, почему бы не делегировать это полностью загруженному объекту, который возвращает полное изображение.Isolate?

В заключение

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

Также важно помнитьFlutter(Dart)Даодин поток, поэтому, чтобы угодить пользователю, разработчик должен обеспечить максимально плавную работу приложения.Futureа такжеIsolateявляются очень мощными инструментами, которые могут помочь вам достичь этого.

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

Если вы обнаружите ошибки в переводе или в других областях, требующих доработки, добро пожаловать наПрограмма перевода самородковВы также можете получить соответствующие бонусные баллы за доработку перевода и PR. начало статьиПостоянная ссылка на эту статьюЭто ссылка MarkDown этой статьи на GitHub.


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из ИнтернетаНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,товар,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.