Неплохая практика - просто начать новые потоки для блокировки операций (Perl)

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

Что такое блокировка операций? Скажем, у меня есть список из 1000 URL-адресов. Я делал следующее:

(Пожалуйста, не возражайте против синтаксических ошибок, я просто издевался над этим)

my @threads;
foreach my $url (@urlList) {    
     push @threads, async {
         my $response = $ua->get($url);
         return $response->content;   
     }
}

foreach my $thread (@threads) {
    my $response = $thread->join;
    do_stuff($response); 
}

Я по существу отбрасываю столько потоков, сколько есть URL-адресов в списке URL-адресов. Если есть миллион URL-адресов, тогда будет выпущено миллион потоков. Является ли это оптимальным, если не оптимальным числом потоков? Является ли использование потоков хорошей практикой для ЛЮБЫХ блокирующих операций ввода-вывода, которые могут ждать (чтение файла, запросы к базе данных и т.д.)?

Связанный бонусный вопрос

Из любопытства потоки Perl работают так же, как Python и GIL? С помощью python, чтобы получить преимущество многопоточности и использовать все ядра для задач с интенсивным процессором, вы должны использовать многопроцессорность.

Ответ 1

Из любопытства потоки Perl работают так же, как Python и GIL? С помощью python, чтобы получить преимущество многопоточности и использовать все ядра для задач с интенсивным процессором, вы должны использовать многопроцессорность.

Нет, но вывод одинаков. Perl не имеет большого замка, защищающего переводчика по потокам; вместо этого он имеет дублирующий интерпретатор для каждого потока. Поскольку переменная принадлежит интерпретатору (и только одному интерпретатору), по умолчанию между потоками не разделяются данные. Когда переменные явно разделены, они помещаются в общий интерпретатор, который сериализует все обращения к общим переменным от имени других потоков. В дополнение к проблемам памяти, упомянутым здесь другими, существуют также серьезные проблемы с производительностью с потоками в Perl, а также ограничения на данные, которые могут быть разделены и что вы можете с ним делать (см. perlthrtut для получения дополнительной информации).

Результат: если вам нужно распараллелить много ввода-вывода, и вы можете сделать его неблокирующим, вы получите намного больше производительности из модели цикла событий, чем потоки. Если вам нужно распараллелить материал, который нельзя сделать неблокирующим, вам, вероятно, повезет больше с несколькими процессами, чем с потоками perl (и как только вы знакомы с таким кодом, отладки).

Также возможно объединить две модели (например, приложение с событием в основном однопроцессорное, которое пропускает некоторую дорогостоящую работу для дочерних процессов, используя POE:: Wheel:: Run или AnyEvent:: Run, или многопроцессорное приложение, у которого есть родительский объект, управляющий событиями, не связанный с событием, или установка типа кластера Node, где у вас есть несколько предварительно настроенных веб-серверов, с родителем, который просто accept и передает FDs своим дочерним элементам).

Нет серебряных пуль, хотя, по крайней мере, еще нет.

Ответ 2

Отсюда: http://perldoc.perl.org/threads.html

Потребление памяти

В большинстве систем частое и непрерывное создание и уничтожение потоков может привести к все большему росту объема памяти интерпретатора Perl. Пока просто запускать потоки, а затем → join() или → detach(), для долгоживущих приложений лучше поддерживать пул потоков и повторно использовать их для необходимой работы, используя очереди для уведомления потоков ожидающей работы. Распределение CPAN этого модуля содержит простой пример (примеры/pool_reuse.pl), иллюстрирующий создание, использование и мониторинг пула многократно используемых потоков.

Ответ 3

Посмотрите на свой код. Я вижу три проблемы:

  • Легко: сначала используйте ->content вместо ->decoded_content(charset => 'none').

    ->content возвращает тело необработанного HTML-ответа, которое бесполезно без информации в заголовках для его декодирования (например, оно может быть gzipped). Он работает иногда.

    ->decoded_content(charset => 'none') дает вам фактический ответ. Он работает всегда.

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

    Самое простое решение - разместить ответы в объекте Thread:: Queue:: Any.

    use Thread::Queue::Any qw( );
    
    my $q = Thread::Queue::Any->new();
    
    my $requests = 0;
    for my $url (@urls) {
       ++$requests;
       async {
          ...
          $q->enqueue($response);
       };
    }
    
    while ($requests && my $response = $q->dequeue()) {
       --$requests;
       $_->join for threads->list(threads::joinable);
       ...
    }
    
    $_->join for threads->list();
    
  • Вы создаете много потоков, которые используются только один раз.

    Существует значительный объем накладных расходов на этот подход. Общей практикой многопоточности является создание пула постоянных рабочих потоков. Эти работники выполняют любую работу, а затем переходят на следующую работу, а не выходят. Работа в пуле, а не конкретный поток, чтобы можно было начать работу как можно скорее. В дополнение к удалению накладных расходов на потоков, это позволяет контролировать количество потоков, выполняемых одновременно. Это отлично подходит для задач, связанных с CPU.

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

    Есть намного лучшие системы для асинхронного ввода-вывода, но они не всегда легко доступны из Perl. В вашем конкретном случае, однако, вам гораздо лучше избежать потоков и перейти с Net:: Curl:: Multi. Следуйте примеру в Synopsis, и вы получите очень быстрый движок, способный выполнять параллельные веб-запросы с очень небольшими накладными расходами.

    Мы, бывший мой работодатель, перешли на Net:: Curl:: Multi без проблем для высокопроизводительного критически важного веб-сайта, и нам это нравится.

    Легко создать оболочку, которая создает объекты HTTP:: Response, если вы хотите ограничить изменения окружающего кода. (Это было для нас.) Обратите внимание, что это помогает иметь ссылку в базовую библиотеку (libcurl), поскольку код Perl является тонкий слой над базовой библиотекой, поскольку документация очень хорошая, и поскольку он документирует все параметры, которые вы можете предоставить.

Ответ 4

Возможно, вы просто захотите рассмотреть неблокирующий пользовательский агент. Мне нравится Mojo::UserAgent, который является частью Mojolicious люкс. Возможно, вам стоит взглянуть на пример, который я высмеивал для неблокирующего искателя для другого вопроса.