Пользователь
0,0
рейтинг
30 июля 2012 в 13:45

Разработка → Возвращаясь к многозадачности на PHP из песочницы

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)
{
  switch($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 task_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'];

  switch($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
карма
5,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (26)

  • +16
    А меня удручает, что пытаются найти всякие пути решения задач на PHP, для которых он не предназначен
    • 0
      Есть такая штука — требования заказчика. И вот иногда они не совпадают с представлениями исполнителя о том насколько предоставленные ресурсы предназначены или не предназначены.
      Вообще само это решение было придумано лет 10 назад для одного из проектов на С.
      И все там было предназначено. Кроме железа которое не успевало выполнить задачу в требуемые сроки с использованием предназначенных механизмов.
      И что бы решить ту задачу и был придуман такой механизм.
  • +4
    А в чем собственно заключается многозадачность?
    • 0
      В том что множество задач выполняются одновременно.
  • +3
    К тому примеру что вы написали, лучше всего подходит библиотека libevent
    • –5
      Может быть. Но у нее есть одно очень существенное ограничение. Это расширение. И его нужно установить. А это не всегда возможно. Т.е. это решение для частного случая когда такая возможность есть. А что делать когда такой возможности нет?
      • +6
        Чтож увас за заказчик такой? Он хочет запустить на шаред хостинге за 2 бакса космический корабль? И что это за задача(или железо), позвольте узнать.
        Хотя конечно сдуру можно и хуйэлоу уорлд написать так, чтоб суперкомпьютер считал два дня.
        • 0
          очень часто жадность предела не имеет и как-то так и хотят, печально конечно еще и то, что многие вместо того что космический корабль за две недели не построить и с коленки не запустить беруться. Ну а на шаред хостинге оно не будет работать банально из-за органичений по времени выполнения сркипта, потребляемой памяти и т.п., но осоздание того что надо было делать таки по-нормальному по законам жанра должно придти когда будет уже вбухано много денег, времени, нервов, написано куча костылей, нагерерировано данных и пр.
          • 0
            Про время скрипта это понятно, а причем тут потребляемая память? Ведь это один скрипт, а создание новой задачи это просто добавление элемента в массив.
            • 0
              только как пример, не влезение в лимит памяти — очень частая проблема с шаред хостингами, есть еще много других конечно:)
        • 0
          В задаче для которой это было придумано заказчик уже закупил железо по смешным ценам. Только оно было настолько тормозное что обрабатывать события стандартными средствами не успевало.
          Но, вообще, это не имеет отношения к сути вопроса.
          • +3
            Еще как имеет. Если обобщить, то суть вопроса выглядит так:
            — Нарисуй мне картину, красивую.
            — Но на вашей машине только 32 мегабайта памяти! Да и планшет не подключается, нет USB порта.
            — Я все понимаю, но у меня есть MS Paint, я сам видел на ютубе как чуваки в нем мышкой мону лизу рисовали.
            — Я конечно могу попробовать, но это будет очень долго и дорого
            — Нет ты знаешь, денег у меня особо нету, да и сделать надо было уже вчера.

            <следующая фраза диалога за вами>
            • –3
              К вопросу о реализации на PHP — заказчиком был я сам. Для себя.
              Я решил что мне нужен простой и удобный механизм для описания задач, их запуска и контроля.
              И я его реализовал. Хотя первые версии были очень неудобные для практического применения.
              Примерно как в примере. Когда появилась довольно удобная версия я стал её использовать.
              Но это статья о самом принципе. Какой смысл обсуждать личные свойства заказчиков?
    • +3
      Вот именно.

      По сути описан слегка завуалированный event loop с callback-ами, вызывающимися на каждом «шаге».
      Не совсем правильно называть это многозадачностью в классическом понимании.

      > Задача это алгоритм обрабатывающий СОБЫТИЯ
      Как раз отсюда и следует, что нам нужно обрабатывать много событий псевдопараллельно — значит, нам нужен цикл обработки событий.

      Кстати, есть небольшая проблема в описанном подходе. Поскольку время у вас дискретно с шагом 10мс, то порядок обработки пришедших событий определяется исключительно порядком запуска задач, а не порядком возникновения событиый — то есть если одно событие произошло раньше другого, то совсем не следует, что оно будет обработано раньше.
      • –1
        Это верно. По сути это и есть цикл обработки событий. А в качестве обработчика и выступает та или иная задача. Только это ведь не настоящая асинхронная обработка событий.
        Это просто быстрая обработка событий. Очень быстрая для PHP.
        Если 10мс кажется много по требованиям задачи, то можно и убрать эту задержку, тогда цикл будет от силы сотни микросекунд (при разумном количестве задач). Только процессор будет сильно загружен.
  • +9
    Поздравляю, вы только что сделали свою первую(?) операционную систему с кооперативной многозадачностью с помощью инструментов, которые для этого совершенно не предназначены. Этому концепту уже далеко не десять лет, он применялся в таких замечательных ОС как Windows до 95 а так же в мириадах встроенных систем.

    Беда в том, что это не настоящая многозадачность. Это ее костыльная реализация на платформе, которая сама по себе является многозадачной и должна освобождать от необходимости городить костыли. Циферками тут похрустеть не получится — для PHP, для ОС это все равно один поток исполнения. Жонглировать данными — да, разумеется, только следите за глобальными переменными, чтобы не вышло как с Therac-25, ну и используйте асинхронные вызовы, потому что синхронные вызовы будут ломать всю вашу многозадачность тем, что они просто не будут отдавать контроль.

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

    (и запускайте то, что пишете — у вас в коде «swith», а не «switch».)
    • –5
      Это вовсе не беда что это не настоящая многозадачность. Это простой и удобный способ обойти некоторые ограничения существующие в языке. Это просто механизм, который может быть очень полезен для решения определенных задач. Вот и все. У кого есть такие задачи могут им воспользоваться. Или воспользоваться любым другим удобным им методом.
  • +1
    несколько лет получал настоящую многозадачность (ну или на 99% как у взрослых) с форком процессов и обменом данными с помощью PCNTL и обертки из ZendX для нее. Из недостатков: юникс онли и некоторое время на спавн нового процесса, но многопоточность и изящность это дело более-менее компенсировала.

    глобалы, процедурный стиль и лисапеды — дорога в ад
    • –1
      >глобалы, процедурный стиль и лисапеды — дорога в ад
      Точно. Но это же просто демонстрация механизма, а вовсе не готовое решение.
    • +1
      У любого решения есть свои плюсы и минусы.
  • 0
    А чем это отличается от

    <?php

    function a($a) { static $inner = null; if(null===$inner) $inner = $a; echo $inner++; }

    register_tick_function(«a», 1);
    declare(ticks=1);
    while(true) { echo «d»; sleep(1); }

    ?>
    ?
    • –2
      По моему — всем.
      • 0
        По-моему, тоже. Тут можно спокойно работать с любой очередью событий, не останавливая выполнение основной ветки программы. И это не изобретение велосипеда, а общеизвестная возможность.
        • 0
          Во первых, у меня нет основной ветки если не считать бесконечного цикла с poll — там всё является задачами. Во вторых в твоем примере нет никаких задач как задач, это просто дерганье какой то функции в произвольные и совершенно не контролируемые моменты времени.
  • +3
    Большая часть программистов заявляют что многозадачности в PHP нет
    А где эта большая часть это заявила? Как собрана статистика?
  • +1
    Чего только люди не делают, лишь бы только не уходить с работы, где ставят такие идиотские задачи, и не перейти работать на том же erlang'е (да даже на python'е с gevent'ом и т.п.)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.