Различия между версиями 7 и 10 (по 3 версиям)
Версия 7 от 2020-12-10 15:53:50
Размер: 7533
Редактор: FrBrGeorge
Комментарий:
Версия 10 от 2020-12-16 12:46:48
Размер: 8638
Редактор: FrBrGeorge
Комментарий:
Удаления помечены так. Добавления помечены так.
Строка 51: Строка 51:
  * Если слияние вырожденное (1+1), в качестве фьючи передаётся `asyncio.sleep(0)`   * Если слияние вырожденное (1+1), в качестве фьючи передаётся `asyncio.sleep(0)` (вариант: определять это внутри функции слияния и не ждать вовсе)
Строка 53: Строка 53:
  * Фьючи можно положить в массив и научить сопрограмму вычислять, какие именно элементы этого массива — правая и левая фьючи, а какой — фьюча результата
  * Решение при этом будет представлять собой один большой `gather()` на все сопрограммы
   * формально говоря, + один `await фьюча_0_16` или её аналога, но он в нашем случае аналогичен ожиданию __послдеднего__ слияния
  
  '''TODO''' формулировка и формат
  * Фьючи можно положить в словарь и научить сопрограмму вычислять, какие именно элементы этого словаря — правая и левая фьючи, а какой — фьюча результата
  * Решение при этом будет представлять собой один большой `gather()` на все созданные задачи
  * Формат ввода/вывода для Д/З:
   * Ввод: Строка в квадратных скобках из 16 чисел через запятую, пригодная для `eval(input())` для типа `list`
   {{{
[22, 58, 95, 33, 47, 11, 76, 38, 70, 84, 40, 35, 56, 28, 13, 23]
   }}}
   * Вывод: результат работы `print(список)` (со скобками и запятыми)
   {{{
[11, 13, 22, 23, 28, 33, 35, 38, 40, 47, 56, 58, 70, 76, 84, 95]
   }}}
  * (на дом) тесты, не забыть закомментировать отладочный код)
Строка 59: Строка 66:
  '''TODO''' формулировка и формат   * Условие: как в Задаче_1, за исключением того, что на вход подаётся список произвольной длины (обязательна синхронизация посредством фьюч и использование общего `gather()` на все созданные фьючи)
  * Тесты:
   1. Сортированный в ''обратном порядке'' список длины 16
   1. Случайный список длины 65
   1. Случайный список длины 63
   1. Случайный список длины 48
   1. Случайный список длины 1000

12.10 Асинхронные возможности Python

Краткий пересказ теории

  • Асинхронная парадигма (вариант реализации): образующий цикл, события и обратные вызовы
  • Python: вместо обратных вызовов — повторный вход в генератор (см. параметрические генераторы), образующий цикл программируйте сами!
  • Асинхронность

    последовательное выполнение ограниченных yield фрагментов кода различных генераторов в порядке, который определяет программист.

    • Образующий цикл согласно какой-то логике выбирает сопрограмма и даёт команду отчёт = сопрограмма.send(команда) для выполнения очередного фрагмента

    • Сопрограмма получает команду с помощью команда = yield(предыдущий отчёт), анализирует её и выполняет следующий фрагмент кода

В действительности:

  • Образующий цикл (со своей логикой) уже реализован кем-то

  • Инструменты управления этой логикой реализованы тем же автором

  • Поэтому пользователи этой действительности не пишут .send() или явный yield

  • Основная конструкция — результат = yield from генератор, который возвращает то, что у генератора написано в return

Синтаксический сахар и асинхронный протокол:

  • Вместо генератора пишем сопрограму с помощью async def. Это тот же генератор, но в нём нельзя пользоваться yield

  • Вместо yield from пишем результат = await сопрограмма (различие в более удобном синтаксисе и в проверке того, сопрограмма с помощью async def)

  • Всё остальное берёт на себя разработчик инструментария:
    1. Образующий цикл
    2. Инструменты управления образующим циклом, которым разрешено делать прямой yield в него

Asyncio

  1. Напишем программу сортировки слиянием массива из 16 элементов
    • Все 15 слияний делать руками с помощью вызова функции слить(начало1, конец1, начало2, конец2)

    • Для слияния используется общий глобальный второй массив
  2. {i} Перетащим их в структуру asyncio.run()/await 

    • Для наглядности снабдим каждый шаг сортировки await asyncio.sleep(0.1) и выводом номера «задания» (передадим его в качестве пятого параметра)

    • Разницы никакой, всё равно запуск последовательный

  3. Для придания асинхронности надо писать свой mainloop. Но он уже есть — это asyncio.run(). Не надо писать свой mainloop, надо пользоваться asyncio.create_task() и asyncio.gather(все 15 слияний)

    • Получается какая-то каша вместо сортировки, почему?
    • {i} Эшелонируем сортировку с помощью нескольких фрагментов вида create_task()… + gather(все_таски_из фрагмента) (палево — их пять)

  4. {i} Эшелонируем сортировку с помощью задания тайм-аута (в пятом параметре будем передавать номер эшелона n и ждать n/c секунд) и общего create_task() / gather()

    • Чем этот подход плох?
  5. Фьюча (future).
    • Пример
    • Алгоритм работы
      1. Awaitable-объект, у которого есть творец и адресат, оба имеют к нему доступ.
      2. Адресат делает результат = await фьюча, и уходит в mainloop.

      3. Творец, когда ему заблагорассудится, с помощью фьюча.set_result(что-то) объявляет mainloop-у, что будущее наступило с результатом что-то

      4. Тогда mainloop, наконец, активизирует эту фьючу и адресат просыпается
    • В действительности, фьюча — это просто генератор на два шага: будушее не наступило / yield / будущее наступило, в котором есть два дополнительных поля (объявление о том, что оно наступило и передаваемое значение)
  6. <!> Задача_1: переписать сортировку с использованием future для синхронизации

    • Каждая функция слияния работает так
      • Дожидается фьючи конца слияния правой половины
      • Дожидается фьючи конца слияния левой половины
      • Выполняет слияние
      • Выставляет готовность фьючи конца слияния своего интервала
    • Если слияние вырожденное (1+1), в качестве фьючи передаётся asyncio.sleep(0) (вариант: определять это внутри функции слияния и не ждать вовсе)

    • Фьючи можно делать просто по именам (7 штук, типа фьюча_4_8) и передавать их в качестве параметров сопрограмме слияния

    • Фьючи можно положить в словарь и научить сопрограмму вычислять, какие именно элементы этого словаря — правая и левая фьючи, а какой — фьюча результата
    • Решение при этом будет представлять собой один большой gather() на все созданные задачи

    • Формат ввода/вывода для Д/З:
      • Ввод: Строка в квадратных скобках из 16 чисел через запятую, пригодная для eval(input()) для типа list

        [22, 58, 95, 33, 47, 11, 76, 38, 70, 84, 40, 35, 56, 28, 13, 23]
      • Вывод: результат работы print(список) (со скобками и запятыми)

        [11, 13, 22, 23, 28, 33, 35, 38, 40, 47, 56, 58, 70, 76, 84, 95]
    • (на дом) тесты, не забыть закомментировать отладочный код)
  7. <!> Задача_2: (на дом). Переписать сортировку под произвольный (не слишком большой) объём данных

    • Условие: как в Задаче_1, за исключением того, что на вход подаётся список произвольной длины (обязательна синхронизация посредством фьюч и использование общего gather() на все созданные фьючи)

    • Тесты:
      1. Сортированный в обратном порядке список длины 16

      2. Случайный список длины 65
      3. Случайный список длины 63
      4. Случайный список длины 48
      5. Случайный список длины 1000

LecturesCMC/PythonIntro2020/Prac/14_Async (последним исправлял пользователь FrBrGeorge 2020-12-16 12:46:48)