6 Ноябрь 2007...22:39

Опять многопоточность, но теперь в PERL (часть 2-я)

Перейти к комментариям

Преамбула

Вот уже целый месяц нет ничего новенького про всеми любимую многопоточность (впрочем, не только про неё). Придётся срочно исправляться.

Итак, поле для манёвров готово, можно продолжать наши игры. Про терминологию напоминать не буду, просто сошлюсь на пост месячной давности, в котором упомянул про threads и forks. Впредь предлагаю к этой теме не возвращаться :) .

С места в карьер

Теперь собственно о том, как программировать разделение на нити. Начну сразу с примера:

#!/usr/bin/perl -w
use threads;

# Создаем три параллельных нити
threads->new(\&get_now, 'number one');
threads->new(\&get_now, 'number two');
threads->new(\&get_now, 'number three');

sub get_now
{
    my $arg = shift;
    print "Thread ", $arg, ": time = ", time(), "\n";
    sleep 1;
}

Пробежимся по коду:

  1. Директива use threads подключает объектно-ориентированную обёртку для работы с нитями (далее будем использовать это слово) приложения. В общем, с этим всё ясно.
  2. Далее идёт собственно деление основной программы на потоки. В нашем примере создаётся 3 нитки, в каждой из которых мы вызываем процедуру get_now(), которая, как Вы догадались, выводит некоторую строку, текущий UNIX TIMESTAMP и ложится спать на 1 секунду.

Запустив код, Вы увидите примерно следующее:
%perl threads.pl
Thread number one: time = 1194392538
Thread number two: time = 1194392538
A thread exited while 4 threads were running.

Как видите, таймштампы одинаковые, из чего можно заключить, что вторая нить запустилась, не дожидаясь окончания работы первой, которая, в свою очередь, «засыпает» на одну секунду (собственно, sleep в get_now() был поставлен именно для того, чтобы нить выполнялась некоторое ощутимое время). Однако у нас не всё благополучно: вместо выхода третьей ветки появилось какое-то странное сообщение. Его природу рассмотрим чуть ниже, а пока порадуемся полчаса: многопоточность работает :) .

О начальниках и подчинённых

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

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

Теперь вернёмся к сообщению об ошибке из предыдущего раздела:

A thread exited while 4 threads were running.

Это сообщение как раз и означает, что основная программа завершилась раньше, чем отработали все нити (начальник после раздачи заданий спешно покинул работу). Можно показать это на примере, внеся небольшие коррективы в код примера:

#!/usr/bin/perl -w
use threads;

# Создаем три параллельных нити
threads->new(\&get_now, 'number one');
threads->new(\&get_now, 'number two');
threads->new(\&get_now, 'number three');

sleep 2;
print "Патрон ушёл домой\n";

sub get_now
{
    my $arg = shift;
    print "Thread ", $arg, ": time = ", time(), "\n";
    sleep 1;
}

Теперь результат работы скрипта будет таким:

%perl threads.pl
Thread number one: time = 1194394574
Thread number two: time = 1194394574
Thread number three: time = 1194394574
Патрон ушёл домой

В этом случае, как видите, основной скрипт засыпает на 2 секунды (время с хорошим запасом, достаточное для того, чтобы все нити успели отработать) и всё заканчивается благополучно.

Из этого всего можно сделать вывод, что Perl по умолчанию ведёт себя ведёт как начальник-халтурщик. Нас это, разумеется, не устраивает, поэтому я продолжаю читать мануал вслух :) . Оказывается, наш начальник может быть многоликим, как небезызвестный Анус Янус: строгим или добрее дедушки Ленина. А может вообще к каждому подчинённому относиться по-своему.

Если без метафор, то это означает следующее: при создании потока, можно указать интерпретатору Perl, ожидать ли завершение работы ветки или создать и забыть. Реализация поведения проста: достаточно при создании нити вызвать метод join() для ожидания завершения работы или же dettach() для отказа управления

Сразу приведу примеры: вот так выглядит вызов метода join() каждой нити:

#!/usr/bin/perl -w
use threads;

# Создаем три параллельных нити
threads->new(\&get_now, 'number one')->join();
threads->new(\&get_now, 'number two')->join();
threads->new(\&get_now, 'number three')->join();

sub get_now
{
    my $arg = shift;
    print "Thread ", $arg, ": time = ", time(), "\n";
    sleep 1;
}

Результат обескураживает:

% perl threads.pl
Thread number one: time = 1194395930
Thread number two: time = 1194395931
Thread number three: time = 1194395932

Вдумчивый читатель обязательно задаст вопрос: «А где же здесь многопоточность?». И будет абсолютно прав, потому что в данном примере многопоточности как таковой нет. Но ещё не вечер, читайте далее, наша цель — расставить все точки над «ё» :) .

А вот пример вызова dettach() при создании нити:

#!/usr/bin/perl -w
use threads;

# Создаем три параллельных нити
threads->new(\&get_now, 'number one')->dettach();
threads->new(\&get_now, 'number two')->dettach();
threads->new(\&get_now, 'number three')->dettach();

sub get_now
{
    my $arg = shift;
    print "Thread ", $arg, ": time = ", time(), "\n";
    sleep 1;
}

Как это выглядит в консоли:

%perl threads.pl
Thread number one: time = 1194396131
A thread exited while 2 threads were running.

В общем, как и ожидалось, основная программа отработала раньше нитей.

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

Ход конём

Решение напрашивается само собой: сначала нужно создать требуемое количество нитей, а потом, уже после их запуска, у каждой из них вызывать метод join(). Как это реализовать? Ход конём — сохраним объекты нитей в массиве (комментарии будут прямо в коде):

#!/usr/bin/perl -w
use threads;

# В этом массиве будут храниться ссылки на
# созданные нити
my @threads;

# Создаём 3 нити в режиме по прниципу "создал и забыл", тем
# самым позволив открыть  параллельно несколько нитей. Объект
# каждой созданной нити помещается в массив @threads
for my $i (1..3) {
  push @threads, threads->create(\&get_now, $i);
}

# Нити успешно созданы,  ссылки на объекты помещены в массив
# Теперь мы можем для каждого объекта вызвать метод join(),
# заставляющий интерпретатор ожидать завершение работы треда.

foreach my $thread (@threads) {
    # Обратите внимание, что $thread является не объектом, а ссылкой,
    # поэтому управление ему передано не будет.
    $thread->join();
}

sub get_now
{
    my $num = shift;
    print "thread ", $num, " => ", time(), "\n";
    sleep 1;
}

Вот теперь всё работает так, как и ожидалось.

% perl threads.pl
thread 1 => 1194388015
thread 2 => 1194387015
thread 3 => 1194387015

Вот теперь, как видите, всё работает как надо :) .

To be continued…

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

  • Вау чувак, спасибо огромное :)
    Ты мне помог очень своей статьёй, пешы, как гаваритцо, ещё :)

  • Оч полезная статья, отличное введение в программирование потоков на перл, многим я думаю пригодится. Пиши ещё.

  • Сабж.
    Есть ли проверенные регистраторы по каталогам, за умеренную плату?. А как там ауторег.ру, жив еще? :)
    Вот тут друг вроде регистрирует у чела, но хз… как-то лениво туда обращаться =) – http://www.top-seo.ru

  • Доброго!
    Все замечательно, но есть но…
    sub get_now
    {
    my $num = shift;
    print «thread «, $num, » => «, time(), «\n»;
    while(1)
    {
    #бесконечный цикл
    }
    }

    Как быть в такой ситуации???
    Ведь пока первая задача не закончиться второя так и будт висеть :(

  • Почему? :-)

    При создании ветки в режиме «создал и забыл» время работы треда не имеет значение :-)

  • Хорошо описано, только нужно ещё обнулять @threads, иначе будет ругаться что уже joined (если потоки будут в цикле создаваться).
    Вопрос вот в другом, допустим, что каждый поток качает и обрабатывает скачанное. Основная программа потоки создаёт только «пачками». То есть если 1 поток наткнётся на медленный сайт – весь скрипт остановится пока не отработает поток. Как бы этого избежать….

  • Хорошо описано, только нужно ещё обнулять @threads, иначе будет ругаться что уже joined (если потоки будут в цикле создаваться).

    Не совсем понял Вашу мысль, если честно. В моих примерах потоки создаются в циклах :)

    допустим, что каждый поток качает и обрабатывает скачанное. Основная программа потоки создаёт только “пачками”. То есть если 1 поток наткнётся на медленный сайт – весь скрипт остановится пока не отработает поток. Как бы этого избежать….

    Почему? Если не приджойниваться к ветке, то «начальник» не будет дожидаться окончания работы подчинённых. Вы сможете создавать форки ровно до тех пор, пока Вам это будет позволять объём свободной памяти. Поэтому не забывайте прибивать данные в использованных тредах…

  • А зачем создавать @threads??? ведь можно использовать threads->list. Даже в документации есть пример.
    foreach my $thr (threads->list)
    {
    # Don’t join the main thread or ourselves
    if ($thr->tid && !threads::equal($thr, threads->self))
    {
    $thr->join;
    }
    }

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

  • my $arg = shift;
    Це что за гон? Можно и более человечно записать:
    my @args = @_;
    my ($arg1, $arg2) = @_;


Ответить

You must be logged in to post a comment.

Для отправки комментария вы должны авторизоваться.