Возвращаясь к многозадачности на PHP

в 9:45, , рубрики: php, многозадачность, Песочница, Программирование, метки: , ,

Написать эту статью меня побудила совершенно удручающая, на мой взгляд, ситуация с решением многозадачности на языке PHP.
Большая часть программистов заявляют что многозадачности в PHP нет, поэтому не надо и пытаться, а менее зашоренные все же пытаются как нибудь извратиться через запуск множества скриптов или, в лучшем случае, придумывают какие то очень частные решения для распараллеливания скачивания, например.

Статья предназначена для демонстрации самой идеи как вообще реализовать многозадачность практически на любом языке программирования.
Так сказать Proof of concept.
Как говорится всё новое это хорошо забытое старое.

Хотя имеется законченная реализация этого метода, сразу предупрежу что давать готовое решение я не буду.
В чем именно возникает проблема организации многозадачности в большинстве языков программирования и в частности в PHP?

Рассмотрим простой пример:

Требуется написать функцию которая ждет определенный интервал времени.

function wait($delta)
{
  $time=microtime(1)+$delta;
  while($time <= microtime(1)) {}
}

Всё просто как грабли, но возникает проблема — эта функция не вернет управление пока не истечет указанное время.
Вот если бы мы могли бы выйти из цикла, а потом вернуться туда снова…
В принципе можно рассматривать эту функцию как задачу, только беда в том, что 2 таких задачи параллельно не запустить.
А чем собственно отличается функция wait от задачи wait? По сути ничем.
Любая задача может быть написана как одна функция, но что же тогда делает функцию задачей?
Правильно! Задача это алгоритм обрабатывающий СОБЫТИЯ. Вот что собственно и делает функцию задачей.
А что делает задача большую часть времени? Опять угадали — ждет какого то события. Значит надо придумать как ждать не ожидая.
А в чем проблема? Посмотрим на это несколько по-другому.

По сути любую задачу можно представить в виде конечного автомата и в данном примере этот автомат имеет всего 3 состояния.
Вот что нам было бы нужно.

function wait($delta)
{
  swith($state)
  {
    case 'init': 
      $time=microtime(1)+$delta;
      $state='wait';
      break;

    case 'wait':
      if($time <= microtime(1))
      {
        $state='exit';
      }
      break;

    case 'exit': 
      // тут бы надо закончить задачу
      // хотя ничто не мешает закончить её в case 'wait', но это для наглядности
      break;
  }
}

При данном описании видно, что функция вернет свое управление сразу и ничего не будет ждать.
Предположим что при первом вызове функции всегда $state='init';
Если регулярно вызывать эту функцию, то она пройдет все свои состояния, при этом сколько бы
времени она не находилась в каком то состоянии времени она практически не занимает, поскольку при каждом вызове она делает очень небольшое количество операций.

Осталось решить вопросы как инициализировать переменную $state, как сохранять локальные перменные между вызовами функции, как вызывать эту задачу и как прекратить ее выполнение?

Начнем с вызова.
заведем массив который будет содержать задачи для исполнения

$tasks=array();

Тогда создание задачи будет сводиться к добавлению необходимых данных в этот массив.
Для простоты предположим, что задача будет принимать всего 1 параметр. На самом деле этого достаточно, хотя и не очень удобно.


function startTask($name,$param)
{
  global $tasks;
  // создаем массив который будет содержать локальные переменные для задачи
  $t=array(
    'name'=>$name,
    'param'=>$param,
    'local'=>array('state'=>'init',), // начальное состояние каждой созданной здачи 'init'
  );
  // и добавляем его в $tasks
  $tasks[]=$t;
  // получаем id задачи
  end($tasks);
  $id=key($tasks);
  return $id;
}

// пишем функцию которая будет удалять задачу из списка
function tasl_end($id)
{
  global $tasks;
  unset($tasks[$id]);
}

// пишем функцию которая будет вызывать все имеющиеся задачи
function poll()
{
  global $tasks;
  foreach($tasks as $id=>$p)
  {
    call_user_func($p['name'],$id,$p['param']);
  }
}

// теперь слегка изменим функцию wait
// теперь она принимает в качестве параметра еще и свой $id 
// что бы иметь возможность получить свои собственные данные
function wait($id,$delta)
{
  global $tasks;
  $p=&$tasks[$id];
  $local=&$p['local'];
  $state=&$local['state'];

  swith($state)
  {
    case 'init': 
      $local['time']=microtime(1)+$delta;
      $state='wait';
      break;

    case 'wait':
      if($local['time'] <= microtime(1))
      {
        $state='exit';
      }
      break;

    case 'exit': 
      // тут бы надо закончить задачу
      // хотя ничто не мешает закончить её в case 'wait', но это для наглядности
      task_end($id);
      break;
  }
}

// теперь запускаем насколько задач
$i=5;
while(i--)
{
  startTask('wait', mt_rand(10,100)/100));
}
// при этом каждая задача wait работает со своими параметрами, своими переменными и заканчивает свою работу независимо от остальных 

// пусть все задачи работают пока не закончатся
while(count($tasks))
{
  poll();
  // обычно не нужно выдерживать интевалы с точностью несколько микросекунд
  // но если нужно максимальное временное разрешение и не жалко ресурсов процессора закоментируйте usleep
  // что бы не сильно напрягать процессор делаем легкую задержку между poll.
  usleep(10000); // отдыхаем 10 миллисекунд
}
echo 'done';

Хочу обратить внимание, что путем модификации $tasks[$id]['name'] и $tasks[$id]['param'] задача может
заставить в следующем цикле выполнять другую функцию вместо текущей. Т.е. функция будет другая, а задача при этом останется та же.

Дальнейшее развитие возможно в сторону более удобных функций для работы с этим механизмом.
Например отсутствие ограничения на количество передаваемых параметров, вызов другой задачи как подзадачи текущей задачи и т.д., но реализацию этих механизмов я оставлю заинтересованным читателям.

Работает это все просто безумно быстро.
И хочу заметить, что все вышенаписанное это не готовое решение, а чисто демонстрация самого подхода.
Но его применение открывает широчайшие возможности. Например использование multi_curl или сокетов оформленных как задачи позволяет запускать множество параллельных задач скачивания, при этом каждая может иметь свои обработчики результатов и свою логику работы с данными, кроме этого другие задачи в это же время могут заниматься другими делами.

Можно писать демоны на PHP и даже драйвера некоторого оборудования.
Например в одном из проектов демон обслуживал несколько устройств на COM портах, каждое из которых имело собственный протокол обмена данными, являлся TCP сервером для внешних клиентов, выступал в качестве клиента для центрального сервера и реализовывал всю логику управления платежным терминалом и при этом занимал всего 3% ресурсов процессора.
И все это делал всего один запущенный скрипт.

С многозадачностью все это становится очень просто в реализации.

Бывают случаи когда какая то операция занимает очень много (по меркам скорости выполнения других операций) времени.
Тогда целесообразно запускать их как внешние процессы, но при этом все равно оформлять их запуск и контроль их исполнения как задачи.

Демка показывающая реальные данные работы аналогичного примера,
находится здесь http://tester.in/rt/task_test.php.

Автор: tester_9

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js