Javascript — решение асинхронной проблемы?
- вторник, 23 января 2018 г. в 03:13:37
В этой статье, я хочу рассказать про свое решение проблемы с асинхронной функциональностью javascript, по средствам введения полностью асинхронной модели вычислений. Будет описана сама концепция, и дана ссылка на реализацию. Заинтересовавшихся прошу под кат.
Начнем с главного — синхронно или асинхронно? Когда имеется синхронный процесс вычисления и асинхронная функциональность без которой не обойтись, это проблема. Более того, когда асинхронность более выигрышна и вообще best practice то эту проблему надо решать.
Что уже есть для решения? Есть коллбэки, есть промайсы. Но промайсы — это модифицированные коллбеки и только упрощают проблему, но не решают её. Для действительного решения проблемы, необходимо сводить все к одной модели — или полностью синхронной или полностью асинхронной.
В последних стандартах, появились свои промайсы, а потом и async/await, что вкупе позволяет свести вычислительный процесс к полностью синхронной модели. И казалось бы, что проблема решена, но у меня есть ряд претензий к данному решению:
Давайте забудем про async/await, забудем про промайсы, как это должно выглядеть? Удобно, единообразно, с минимум дополнительного кода? Что то вроде функции, только асинхронной:
То есть, хорошо бы превратить синхронную функцию JS в асинхронную. Давайте составим список шагов для выполнения данной задачи.
любой ветви всегда была в работе, что бы поток исполнения гарантированно мог на нее вернутся в случае ухода.call и back.Давайте попробуем это все изобразить. По условию определение асинхронной функции, должно быть ровно такое же, как и синхронной.
function A ()
{
}Пусть call:
Пусть back:
Тут выполняется первых 2 пункта:
function A ( top )
{
}Пункт три, как уже было описано выше, специальный метод-обертка back, при вызове (команде), переходит на один узел назад по дереву вызовов. Таким образом и осуществляется асинхронный возврат. Тут же условимся что синхронный возврат (return) больше не учитывается, и асинхронный вызов продолжает существовать даже после синхронного возврата.
Из вышеописанного становится понятно что вызовов конкретной синхронной функции, превращенной в асинхронную, будет не один а несколько:
callbackНужен способ ловить, или же вычленять куски кода нужные при каждом конкретном вызове, и переносить поток исполнения именно туда. К тому же нужна возможность сохранять данные между подвызов-возврат, так как контекст исполнения пропадает после каждого синхронного возврата. Введем следующее:
top будет введено поле markcall ставить mark равной #back ставит mark равной имени функции из текущей вершины (которая удаляется)switch осуществляет направление потока исполненияback), записываются прямо в top.x = 1;function A ( top )
{
switch ( top.mark )
{
case '#':
break;
case 'B':
break;
case 'C':
break;
}
}Собираем все в месте:
call ставит переданные аргументы в top.argback ставит переданный результат в top.retfunction A ( top )
{
switch ( top.mark )
{
case '#':
call(top, 'B', 'some_data');
break;
case 'B':
call(top, 'C', top.ret + 1);
break;
case 'C':
back(top, {x: top.ret});
break;
}
}Из примера выше видно, что получается обычная синхронная функция, только растянутая, и с возможностью дождаться асинхронного действия перед возвратом. Так же можно заметить, разбиение на шаги, и их последовательное исполнение. Учтем это, и то что используется дерево вызовов, а не стек, и добавим параллельности:
top будет введено поле sizecall увеличивает size текущей вершины на единицуback уменьшает size предыдущей вершины на единицу (текущая уничтожается)function A ( top )
{
switch ( top.mark )
{
case '#':
top.buf = [];
call(top, 'B', 'some_data1');
call(top, 'B', 'some_data2');
call(top, 'B', 'some_data3');
break;
case 'B':
top.buf.push(top.ret);
if ( !top.size )
{
call(top, 'C', top.buf);
}
break;
case 'C':
back(top, top.ret);
break;
}
}Получается возможность запуска массивной параллельной задачи, на любом последовательном шаге общего асинхронного процесса исполнения функции. Так же возможность дождаться завершения этой задачи и накопить результат. Давайте это улучшим, а именно учтем то, что не всегда нужен результат top.ret конкретной функции, и было бы неплохо иметь возможность параллельного запуска различных функций в одной задаче:
top будет введено новое поле groupmark будет заменено на group['#name']size будет заменено на group['#size']call будет введен новый параметр groupMarkcall увеличивает top.group[groupMark] текущей вершины на единицуback уменьшает top.group[groupMark] предыдущей вершины на единицу (текущая уничтожается)call и back управляют значением специальных имен top.group['#name'] и top.group['#size'] содержащих имя и размер текущей группыfunction A ( top )
{
switch ( top.group['#name'] )
{
case '#':
top.buf = [];
call(top, '#group1', 'B1', 'some_data1');
call(top, '#group1', 'B2', 'some_data2');
call(top, '#group1', 'B'3, 'some_data3');
break;
case '#group1':
top.buf.push(top.ret);
if ( !top.size )
{
call(top, '#group2', 'C', top.buf);
}
break;
case '#group2':
back(top, top.ret);
break;
}
}Добавим еще возможность дождаться окончания нескольких запущенных групп, и на этом все :)
top.group['##size'] будет содержать сумму всех top.group['#size']top.group['##name'] содержащая имя группы с которой была вызвана данная функция (имя группы возврата)function A ( top )
{
switch ( top.group['#name'] )
{
case '#':
top.listB = [];
top.listC = [];
call(top, '#group1', 'B1', 'some_data1');
call(top, '#group1', 'B1', 'some_data1');
call(top, '#group1', 'B2', 'some_data2');
call(top, '#group2', 'С1', 'some_data1');
call(top, '#group2', 'С1', 'some_data1');
call(top, '#group2', 'С2', 'some_data2');
break;
case '#group1':
if (top.ret) {top.listB.push(top.ret);}
if ( !top.group['##size'] )
{
back(top, {B: top.listB, C: top.listC});
}
break;
case '#group2':
if (top.ret) {top.listC.push(top.ret);}
if ( !top.group['##size'] )
{
back(top, {B: top.listB, C: top.listC});
}
break;
}
}Выше описана концепция асинхронной функции, позволяющая ввести полностью асинхронную модель вычисления, и при этом:
Мной разработана довольно удачная реализация данной концепции, с упором на параллельные вычисления. Ключевая особенность — поддержка потоков (WebWorker) и возможность асинхронного вызова функций, в каком бы потоке они не находились.