Многопоточность на PHP

В статье описана организация мультизапросов средствами PHP с использованием библиотеки cURL. Данный механизм предполагается использовать для создания скриптов, осуществляющих автоматизированные запросы ко множеству веб-серверов.

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

В тех случаях, когда необходимо сделать запрос к одному веб-серверу, вполне достаточно обычных средств cURL, однако если требуется формировать большое количество веб-запросов, то применение механизма многопоточности может дать существенный прирост производительности и ускорение работы скрипта.

Прежде чем начать описывать механизм разработки скриптов сначала о том, что же я подразумеваю под многопоточностью. Дело тут в том, что ни какой многопоточности в PHP на самом деле нет и когда употребляется термин «многопоточность» касательно библиотеки cURL, то речь идет о мультизапросах.

Механизм мультизапросов заключается в том, что во время посылки запросов веб-серверам, PHP не дожидается ответа от каждого поочередно посланного запроса, а посылает (опять же поочередно) сразу несколько запросов, и уже после этого обрабатывает приходящие от них ответы. Поэтому применять многопоточность имеет смысл только тогда, когда осуществляются запросы к разным серверам – если необходимо осуществить большое количество запросов к одному серверу, то многопоточность не принесет заметного увеличения производительности скрипта.

Сразу хочу заметить, что средства работы с многопоточностью в cURL весьма скудные, но даже с теми что есть можно организовать полноценную работу с мультизапросами.

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

1. Список всех URI помещаем в массив
2. Создаем массив «обычных» cURL в требуемом количестве (количество потоков) и один cURL_multi
3. Инициализируем каждый созданный cURL (URL из подготовленного ранее массива, переменные post, если требуется, прокси и т.д.)
4. Добавляем каждый cURL в cURL_multi
5. Запускаем все потоки при помощи вызова cURL_multi
6. В цикле опрашиваем состояние cURL_multi и если есть отработавший поток, обрабатываем полученную страницу и на его место запускаем новый cURL. Если список URI закончился, то только обрабатываем результат. Цикл продолжается до тех пор, пока есть хотя бы один незавершенный поток.
7. Закрываем все cURL.

Теперь, собственно, скрипт который выполняет данную операцию:

  1. <?php
  2. function Parse(&amp;$urls,$flowcount) {
  3.   // $urls — массив с URL-адресами
  4.   // $flowcount — количество потоков
  5. //Запуск потоков
  6.   $ch=array();
  7.   $lcount0=count($urls);
  8.   if($flowcount>$lcount0) $flowcount=$lcount0;
  9.   for($flow=0;$flow<$flowcount;$flow++) $ch[]=curl_ini(array_pop($urls))//создание массива cURL
  10.   $mh=curl_multi_init()//создание cURL_multi
  11.   for($flow=0;$flow<$flowcount;$flow++) { //В этом цикле инициализируются cURL
  12.     curl_setopt($ch[$flow],CURLOPT_REFERER,‘TESTREFERER’);
  13.     curl_setopt($ch[$flow],CURLOPT_USERAGENT,);
  14.     curl_setopt($ch[$flow],CURLOPT_RETURNTRANSFER,1);
  15.     curl_setopt($ch[$flow],CURLOPT_POST,1);
  16.     curl_setopt($ch[$flow],CURLOPT_POSTFIELDS,‘TEST=TESTVAR’);
  17.     curl_setopt($ch[$flow],CURLOPT_COOKIE,‘TEST=TESTCOOKIE’);
  18.     curl_multi_add_handle($mh,$ch[$flow]);
  19.   }
  20.   $flows=null;
  21.   do { //Основной цикл, продолжается до тех пор, пока есть хотябы один работающий поток
  22.     do curl_multi_exec($mh,$flows)while($flows==$flowcount)//циклическая проверка количества работающих потоков
  23.     $info=curl_multi_info_read($mh);
  24.     if(!count($urls)) { //Больше нет URL для обработки
  25.       $res=curl_multi_getcontent($info[‘handle’]);
  26.       curl_multi_remove_handle($mh,$info[‘handle’]);
  27.       curl_close($info[‘handle’]);
  28.       $flowcount–;
  29.     } else { //Есть еще URL для обработки
  30.       curl_setopt($info[‘handle’],CURLOPT_URL,array_pop($urls));
  31.       $res=curl_multi_getcontent($info[‘handle’]);
  32.       curl_multi_remove_handle($mh,$info[‘handle’]);
  33.       curl_multi_add_handle($mh,$info[‘handle’]);
  34.       curl_multi_exec($mh,$flows);
  35.     }
  36.     //Анализ результатов
  37.     $err=AnalizePage($res);
  38.     if($err) die(‘Некая ошибка!’);
  39.   } while($flows>0);
  40.   curl_multi_close($mh);
  41. }
  42. ?>

В тексте кода достаточно комментариев, чтобы разобраться что происходит. Поясню несколько моментов…

1. Вызов curl_multi_init должен быть осуществлен ОБЯЗАТЕЛЬНО после того, как все “обычные” cURL будут проинициализированы, т.е. нельзя поменять 9ю и 10ю строки местами, поэтому участки кода по инициализации $ch[] и задания необходимых параметров разделены.

2. При каждом вызове curl_multi_exec в строке 22 в переменную $flows помещается количество активных потоков, которое далее сравнивается с количеством запущенных потоков (переменная $flowcount будет уменьшаться, если в списке обрабатываемых URL (массив $urls) больше нет записей).

3. curl_multi_info_read возвращает информацию об очередном отработавшем потоке, или false, если с момента предыдущего вызова этой функции никаких изменений небыло.

4. Функция curl_multi_info_read обновляет данные, помещаемые в переменную $info только после того, как будет выполнен curl_multi_exec, поэтому для обработки каждого потока необходимо использовать обе функции.

5. Чтобы добавить новый поток необходимо последовательно выполнить вызов трех функций: curl_multi_remove_handle, curl_multi_add_handle и curl_multi_exec.

Ну и последнее: иногда важно знать какую-либо дополнительную информацию, связанную с обрабатываемым потоком. В этом случае можно создать ассоциативный массив, ключами которого будут являться идентификаторы потока, т.е. значения в $info[‘handle’].