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) больше не учитывается, и асинхронный вызов продолжает существовать даже после синхронного возврата.
Из вышеописанного становится понятно что вызовов конкретной синхронной функции, превращенной в асинхронную, будет не один а несколько:
call
back
Нужен способ ловить, или же вычленять куски кода нужные при каждом конкретном вызове, и переносить поток исполнения именно туда. К тому же нужна возможность сохранять данные между подвызов-возврат, так как контекст исполнения пропадает после каждого синхронного возврата. Введем следующее:
top
будет введено поле mark
call
ставить mark
равной #
back
ставит mark
равной имени функции из текущей вершины (которая удаляется)switch
осуществляет направление потока исполненияback
), записываются прямо в top.x = 1
;function A ( top )
{
switch ( top.mark )
{
case '#':
break;
case 'B':
break;
case 'C':
break;
}
}
Собираем все в месте:
call
ставит переданные аргументы в top.arg
back
ставит переданный результат в top.ret
function 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
будет введено поле size
call
увеличивает 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
будет введено новое поле group
mark
будет заменено на 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) и возможность асинхронного вызова функций, в каком бы потоке они не находились.