Многие разработчики типового говнософта, ориентированного на работу с вебом, зачастую используют потоки для того, чтобы получить выигрыш в скорости. Данный подход, конечно, обладает своими плюсами, но все же не является оптимальным, например, с точки зрения потребляемых ресурсов системы (особенно когда речь идет о потребителях, любящих ставить сразу "тыщу потоков").
Альтернативным и общеизвестным способом ускорения работы софта является асинхронная модель, то есть модель, при которой все вызовы методов являются неблокирующими. В данной статье я рассмотрю простой пример, который будет использовать асинхронные веб-запросы.
В качестве примера будет написан парсер идентификаторов приложений с Android Market, который пригодится в готовящейся статье, посвященной добычи трафика с маркета. Для простоты будем использовать модуль AnyEvent, он упрощает реализацию асинхронной событийной модели. Итак, приступим.
Для начала прагмы, необходимые инклюды и переменные:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
use strict; use warnings; use AnyEvent::HTTP; use Fcntl qw/:flock/; #Список категорий, которые скрипт будет обрабатывать my @cat_list = qw/ARCADE BRAIN CARDS CASUAL GAME_WALLPAPER RACING SPORTS_GAMES GAME_WIDGETS/; #Лимит страниц для каждой категории my $page_limit = 15; #Файл для сохранения результата my $res_file = 'result1.txt'; #Autoflush $| = 1; #Для последующего определения времени работы скрипта my $time = time; |
В целом код довольно небольшой, поэтому я не буду излишне фрагментировать его, а изложу основную часть одним куском и прокомментирую.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
#Перебираем категории в массиве for my $cat_name(@cat_list) { #Создаем переменную, необходимую для последующей блокировки дальнейшего выполнения скрипта #(чтобы выполнять запросы пачками, по 15) #http://search.cpan.org/~mlehmann/AnyEvent-6.14/lib/AnyEvent.pm#CONDITION_VARIABLES my $cv = AnyEvent->condvar; #Ставим в очередь гет-запросы for(my ($i, $j) = (0, 0); $i < $page_limit; $i++) { print $cat_name, ' - ', $i, $/; #Определяем параметры гет-запроса http_get ( 'https://market.android.com/details?id=apps_topselling_paid&cat='.$cat_name.'&start='.($i * 25).'&num=25', headers => { 'User-Agent' => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2)' }, timeout => 5, #Колбэк, который вызывается после выполнения запроса sub { my ($data, $headers) = @_; #Если страница не существует, то android market возвращает 404 #Проверяем этот момент if($headers->{Status} =~ /^200/) { #Сохраняем в файл идентификаторы приложений open F, '>>', $res_file or warn $!; flock F, LOCK_EX; print F (join $/, $data =~ /data-docid="(.+?)"/g), $/; flock F, LOCK_UN; close F; } #Если все страницы были обработаны, if(++$j == $page_limit) { #Снимаем блокировку, чтобы поставить в очередь следующую пачку запросов $cv->send; } } ); } #Блокируем переменную, ждем пока все запросы будут обработаны #(как-бы ждем "сигнала", когда будет вызван метод ->send) $cv->recv; } #Выводим время работы скрипта print 'Time elapsed: ', time - $time, $/; |
Теперь сравним вышеприведенный скрипт со скриптом, который выполняет ту же самую работу, но опирается на потоки. Вот код этого скрипта:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
use strict; use warnings; use threads; use threads::shared; use IO::Socket::SSL; use Fcntl qw/:flock/; my $thr_cnt = 15; my @cat_list : shared = qw/ARCADE BRAIN CARDS CASUAL GAME_WALLPAPER RACING SPORTS_GAMES GAME_WIDGETS/; my $page_limit = 15; my $res_file = 'result2.txt'; $| = 1; my @trl = (); my $w_lock : shared; my $time = time; $trl[$_] = threads->create(\&main) for 0..$thr_cnt - 1; $_->join for @trl; print 'Time elapsed: ', time - $time, $/; sub main { while(1) { my $cat_name = shift @cat_list or last; for(my $i = 0; $i < $page_limit; $i++) { print $cat_name, ' - ', $i, $/; my $socket = IO::Socket::SSL->new ( PeerAddr => 'market.android.com', PeerPort => 443, PeerProto => 'tcp', TimeOut => 5 ); if($socket) { my $req = "GET /details?id=apps_topselling_paid&cat=$cat_name&start=".($i * 25)."&num=25 HTTP/1.0\r\n". "Host: market.android.com\r\n". "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2)\r\n". "Connection: Close\r\n\r\n"; print $socket $req; my $data; while(read $socket, my $buffer, 128) { $data .= $buffer; } close $socket; if($data =~ /200 OK/) { lock $w_lock; open F, '>>', $res_file or warn $!; flock F, LOCK_EX; print F (join $/, $data =~ /data-docid="(.+?)"/g), $/; flock F, LOCK_UN; close F; } } } } } |
Скорость работы скриптов примерно одинакова, и там и там идет одновременное выполнение 15 веб-запросов, однако, если обратить внимание на потребляемую память и нагрузку на процессор, то мы увидим явное преимущество у первого скрипта. У меня количество потребляемой памяти для асинхронного варианта составляло ~ 12 мегабайт, а в случае с потоками оно увеличилось до ~ 53 мегабайт (хотя прожорливость perl + pthreads под win* - известная печалька).
Но даже если абстрагироваться от языка программирования и использовать WinAPI для работы с потоками, то можно увидеть, что при большом количестве потоков (конечно, ведь чем больше потоков, тем круче и быстрее (с) дефолт логика) нагрузка на процессор вырастет несоизмеримо по сравнению с асинхронной моделью.
В общем, асинхронная модель зачастую является более предпочтительной, но иногда требует нетривиального планирования алгоритма работы программы, например, когда есть необходимость совершать множество зависящих друг от друга запросов и менять логику поведения в зависимости от результатов, полученных на предыдущих стадиях.
Скрипты из статьи: скачать
Спасибо за интересный пример, давно хотел попробовать поработать с AnyEvent.
>конечно, ведь чем больше потоков, тем круче и быстрее (с) дефолт логика
Дефолт сарказм обладателя полуядерного ЦП
Риточка, Вы не правы. Если Вы поставите 1000 потоков на четырехядерной машине, Вы уже увидите, что приличная часть процессорного времени расходуется на переключение их контекстов. Если речь идет о Windows и перле, то вы и 100 потоков с трудом поставите, так как у вас оперативной памяти несколько гигабайт отожрется при этом. А если Вы поддерживаете мысль "1000 потоков - это охуенно", то советуем Вам немного пересмотреть свою логику. Хотя, вероятно, Вы просто can't into asynchronous.
Вот она система моей мечты, Windows, конечно! Весь софт серьезные дядьки - спамеры веба пишут под неё, ага.
Серьезные дядьки-спамеры по социалочкам голосуют за Windows из-за наличия кучи дешевых дедиков! Windows - выбор профессионала.
>Дефолт сарказм обладателя полуядерного ЦП
дефолт сарказм владельца мощного пк купленного за мамины деньги.
Что-то я не понял, зачем вам тут (f)lock понадобился. Приложение однопоточное, в каждый момент времени у вас может выполняться только один колбэк. Вопрос: зачем блокировка (еще и двойная)?
ctrl+c - ctrl+v
Добыча трафика с маркета, это что такое? Переманивание посетителей с маркета на какой-то свой сайт? Или раскрутка своего приложения на маркете?
1ое
Kaimi, ты так ненавидишь быдло/говно кодеров, но почему ты сам сам активно занимаешься быдло/говно кодингом за деньги? Или это чисто прагматическая ненависть к конкурентам?
Уходи, а то я тебя по айпи вычислю.
Пощади, о ужасный Ктулху.
А в C# теперь есть async/await, благодаря чему IO completion ports использовать так же удобно, как и потоки.
Подскажите как можно использовать AnyEvent::Socket? На CPAN этот модуль есть, а Perl Packet Manager его не отображает для установки.
http://www.activestate.com/blog/2010/10/how-install-cpan-modules-activeperl