JavaScript async/await функциите са божествено решение на проблема с „обратното извикване по дяволите“. Някои казват, че асинхронните функции причиняват друг проблем, бавно изпълнение, което бихме могли да наречем „async/await hell“. За мен това е прекалено голям проблем, но определено си струва да проучим този проблем с асинхронните функции и решението.

Обратно повикване по дяволите:

Async/Await решение:

Проблемът с решението е, че вашият код прави ... Едно ... Нещо ... В ... A ... Време. Ами ако някои от отделните стъпки могат да се изпълняват паралелно? Паралелното изпълнение на подредени задачи може да бъде печалба в производителността, защото докато една задача е на пауза в очакване на междинен резултат, другата задача(и) може да направи нещо.

Вдъхновение от реалния свят

Наскоро, докато размишлявах върху производителността на генератора на моя статичен уебсайт — AkashaCMS — ми хрумна, че основният цикъл на рендиране е рендиране на … Един … Файл … В … A …. време. Освен това преработването на цикъла за паралелно изобразяване на файловете може да даде тласък на производителността.

Използването на AkashaCMS за изобразяване на уебсайта TechSparx отне над 40 минути за изобразяване на няколкостотин страници, което изглеждаше прекалено. Отчасти виновникът е големият брой вградени видеоклипове в YouTube и необходимостта от извличане на кодове за вграждане за всеки един. Но докато една страница се изобразява и може би е поставена на пауза за извличане на кода за вграждане, друга страница може да бъде изобразена.

Паралелизиране

Ако проблемът е, че последователното изпълнение на асинхронни задачи отнема известно време, решението е изпълнението на задачите паралелно. С други думи, можете да създадете масив от задачи, след което да използвате Promise.all, за да ги изпълните всички паралелно, завършвайки, когато всички обекти на Promise бъдат разрешени.

  • Ами ако имате толкова много задачи, че да взривявате паметта си, за да изпълнявате всички задачи едновременно?
  • Ами ако някои задачи зависят от резултатите от други задачи?

С други думи, Promise.all не е решението за всичко. Ще можете да използвате Promise.all при някои обстоятелства, но не всички.

Бенчмаркинг

Проблемът се свежда до - генериране на масив от файлове за обработка и обработка на всеки файл, приспособявайки асинхронно изпълнение за обработка на всеки файл. Горният кодов фрагмент е резервен, който използвах за сравнителен анализ на производителността. Той прави N копия на файл, едно копие наведнъж.

Нито една от тези задачи не зависи от други задачи. Следователно теоретично е възможно просто да направите масив от задачи на copyFile и да използвате Promise.all, за да изчакате всички да завършат. Но колко задачи на copyFile ще трябва да изпълните и дали копирането на 100 файла наведнъж или 10 000 файла наведнъж ще се вмести в ограниченията на паметта ви?

За базова линия използвах това, за да направя двадесет копия на файл от 1 GB на Intel NUC Core i5 с 16 GB памет на обикновен въртящ се твърд диск. Това отне 9 минути.

След това пренаписах горното, използвайки модула run-parallel-limit за Node.js. Този модул управлява паралелното изпълнение, така че можете да ограничите броя на паралелните задачи, които се изпълняват в даден момент.

Двете програми са предназначени да изпълняват една и съща задача. Първото изпълнение е много по-приятно за четене, но това работи по-бързо.

Моите измервания показаха, че правенето на 20 копия с едновременно изпълнение на 4–5 паралелни задачи отнема 5 минути. Това е голямо подобрение от 9 минути, разбира се.

Измервания в реалния свят

С този положителен резултат в ръка пренаписах цикъла на изобразяване в AkashaCMS. Кодът по-горе е грубо как е конструиран новият цикъл на изобразяване. „В хранилището на Github има подробности.“

След това изобразих уебсайта на TechSparx с различни нива на едновременност.

Той премина от около 45 минути до 11–12 минути чрез конфигуриране за паралелност от около 10 (което означава рендиране на 10 файла наведнъж).

Защо увеличението на производителността е по-рязко? Мисля, че това е свързано с многото вградени видеоклипове в YouTube в съдържанието на TechSparx.

В AkashaCMS изобразяването на файл е по-сложно от простото създаване на копие. Markdown се изобразява в HTML, след което HTML се изпълнява през шаблонна машина, а друга подсистема обработва персонализирани тагове, които управляват различни DOM манипулации. Например потребителският етикет ‹footnote› предизвиква бележка под линия в долната част на страницата, а персонализираният етикет ‹embed-resource› е това, което управлява вграждането на видеоклипове в YouTube.

Извличането на метаданни от YouTube изисква значителна пауза. Както предложих по-рано, по време на тази пауза за извличане на данните за едно видео AkashaCMS може да прави нещо друго. Оттук идва огромната печалба в производителността.

Заключение

Това, което научихме от това упражнение, е да не се заслепяваме от красотата на кодирането с асинхронни функции. Беше скучна работа да напишем нашия асинхронен код с помощта на обратни извиквания. Можем да кажем на нашите внуци, че не знаят колко добре им е било, защото трябваше да изградим цели пирамиди от вложени обратни извиквания, вървейки нагоре в двете посоки в снега.

Въпреки че еквивалентният код, използващ асинхронни функции, е много по-чист, той не е панацея, която да се използва във всеки случай. Примерът тук демонстрира отрицателно въздействие върху производителността от използването на обикновен цикъл.

В общия случай нашите приложения вероятно ще имат комбинация от паралелизирани задачи и непаралелизирани задачи. Следователно трябва да конструираме кода, за да паралелизираме това, което може да бъде паралелизирано, и трябва да сме достатъчно мъдри, за да разпознаем какво може и какво не може да бъде паралелизирано.

Повече информация има в моя блог: https://techsparx.com/nodejs/async/avoid-async-kill-performance.html

Четвъртото издание на моята книга, Node.js уеб разработка, трябва да бъде публикувано скоро. Моята авторска страница в Amazon изброява тази и други мои книги.