openlang

Фильтр стал async, а интерфейс всё равно замирает

Поиск фильтрует список примерно из сорока тысяч строк, и на каждом запросе интерфейс на миг замирает. Я решил, что поможет async, так что фильтр стал таким, а обработчик нажатия ждёт его через await.

Future<List<Entry>> filterRows(List<Entry> rows, String query) async {
  final out = <Entry>[];
  for (final r in rows) {
    if (r.matches(query)) out.add(r);
  }
  return out;
}

// in the tap handler
setState(() => loading = true);
final result = await filterRows(allRows, query);

Замирает так же. Даже спиннер, который я включаю прямо перед вызовом, не появляется. Я знаю про Isolate.run, пользовался им для большого разбора JSON, но для фильтра это показалось перебором, и я думал, что await как раз даёт кадрам рисоваться, пока он ждёт. Что здесь на самом деле делает await?

Переведено с английскогоПоказать оригинал

Разговор продолжается 8

Следуя доводам
  1. Ссылка на ответ

    Я прочитал это так, как читал бы вызов CompletableFuture.supplyAsync в Java. Там тело выполняется в потоке из пула, а вызывающий только ждёт значения, так что медленный цикл внутри никогда не трогает поток интерфейса. Если Future здесь устроен так же, замирает не сам фильтр. Я бы посмотрел на то, что выполняется после await. Перестроить сорок тысяч строк в потоке интерфейса за один раз вполне может стоить столько, и спиннер, возможно, просто проигрывает гонку этой перестройке.

    Переведено с английскогоПоказать оригинал
  2. Ссылка на ответ

    Нет, и при переходе с Java такую связь провести легко. Future в Dart - это значение, которое придёт позже, а не поток. Всё приложение работает в одном изоляте, а у этого изолята один поток, который крутит один цикл событий. Вдобавок async-функция выполняется синхронно до первого await. Внутри filterRows нет ни одного await, так что весь цикл проходит прямо в обработчике нажатия, ещё до того, как Future вообще вернётся. Кадр, который нарисовал бы спиннер, сидит в очереди событий за ним. Перестройка после await тут не медленная часть, она даже не начинается, пока цикл не закончится.

    Чтобы убрать работу из этого цикла, нужен второй изолят.

    import 'dart:isolate';
    
    List<Entry> filterRows(List<Entry> rows, String query) =>
        rows.where((r) => r.matches(query)).toList();
    
    // in the tap handler
    final result = await Isolate.run(() => filterRows(rows, query));

    Теперь await ждёт сообщения от другого изолята, и цикл событий в это время свободен. Хотелось бы, чтобы ключевое слово меньше звучало так, будто оно уносит работу куда-то ещё.

    Переведено с английскогоПоказать оригинал
  3. Ссылка на ответ

    Вы правы, у меня было наоборот. Я перенёс поток из пула от supplyAsync, а это как раз то, чего Dart никогда не обещал. Если перечитать код n.vos с этой поправкой, цикл идёт внутри самого вызова, а перестройка, на которую я грешил, к моменту зависания ещё даже не началась.

    Переведено с английскогоПоказать оригинал
  4. Ссылка на ответ

    Зависит от того, что ожидается. Проследите путь кадра через очереди. await на уже завершённом future продолжается в микрозадаче. Все микрозадачи выполняются до следующего события. Кадр - это событие, так что он по-прежнему ждёт за всем циклом. await Future.delayed(Duration.zero) продолжается из таймера, а таймер - это событие. Теперь цикл уходит в конец очереди событий, и кадр, который уже ждал, получает свой ход первым. Так что версия с порциями продолжает рисовать экран. Но каждая порция всё равно выполняется в UI-изоляте, и слишком длинная порция точно так же роняет кадры.

    Переведено с английскогоПоказать оригинал
  5. Ссылка на ответ

    Сработало, правда, не с первого раза. В моём обработчике allRows - это поле, и в первой попытке я использовал его прямо внутри замыкания. Это захватывает this и тащит в изолят весь State, та же ловушка, что была с моим Loader. Копия в локальную переменную перед Isolate.run всё исправила, и теперь спиннер появляется, а список прокручивается, пока фильтр работает.

    Переведено с английскогоПоказать оригинал
  6. Ссылка на ответ

    Синхронный старт моложе, чем кажется. Ранний Dart делал наоборот, и async-функция сначала возвращала свой Future, а тело начинала выполнять только потом. Dart 2 это поменял, и тело стало выполняться синхронно до первого await (в списке изменений это значилось как ломающее изменение, и справедливо, ведь кое-какой код опирался на то, что тело стартует позже, хотя никто этого не записывал, а новое правило совпало с тем, что async-функции в JavaScript делали с первого дня). При любом из правил цикл n.vos идёт в единственном изоляте, так что экран замирал бы всё равно.

    Переведено с английскогоПоказать оригинал
  7. Ссылка на ответ

    Isolate.run здесь правильный выбор по умолчанию, с одной оговоркой. Всё, что захватывает замыкание, отправляется в новый изолят, так что сорок тысяч строк на каждом запросе уходят туда копией. Результат возвращается через Isolate.exit, который, по документации, передаёт его без копирования, так что платит только вход. Эта цена растёт вместе со списком, и стоит один раз замерить поиск на настоящих данных, прежде чем считать зависание побеждённым.

    Переведено с английскогоПоказать оригинал

Другие стороны того же вопроса

  1. asyncio.TaskGroup, когда один запрос падает

    Сравнение конкурентности в разных языках обычно начинают с синтаксиса. Я бы начал с того, что нужно расчёту, когда что-то пад…

    Ответы 15pythonjuliago
  2. println внутри функции для swap!

    В документации к swap! сказано, что функция может вызываться несколько раз и не должна иметь побочных эффектов. А в реальном…

    Ответы 1clojure
  3. Isolate.run утащил с собой весь Loader

    Вынес большой разбор JSON с главного изолята через Isolate.run и вместо ускорения получил ошибку про объект, который нельзя о…

    Ответы 2dart