Опубликован: 17.08.2006 | Доступ: свободный | Студентов: 5373 / 697 | Оценка: 4.49 / 3.94 | Длительность: 20:58:00
ISBN: 978-5-9556-0078-9
Лекция 16:

Взаимодействие процессов

< Лекция 15 || Лекция 16: 1234 || Лекция 17 >

Многопоточное выполнение - нити

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

Полновесные процессы и нити (потоки управления)

Рис. 16.1. Полновесные процессы и нити (потоки управления)

Существует несколько моделей многопоточной обработки, например DEC, Java, POSIX, Win32. Perl предлагает свою модель многопоточного программирования, отличающуюся от перечисленных и имеющую свои достоинства и недостатки. Появление в Perl кросс-платформенных средств работы с легковесными процессами стало несомненным достижением, которое заставило по-новому взглянуть на программирование параллельных процессов. Применение легковесных процессов позволяет разрабатывать эффективные приложения, одинаково выполняющиеся на разных платформах.

Работать с легковесными процессами просто. Подключив средства работы с нитями прагмой use threads, можно создать нить с помощью метода threads->new (синоним: threads->create ). Этому методу передается ссылка на именованную или анонимную подпрограмму, которая запускается на выполнение в виде параллельного потока управления. Результатом создания нити станет ссылка на объект типа threads, который будет использоваться для управления потоком. Создание нити выглядит так:

use threads; # подключить многопоточные средства
my $thread = threads->new(\&pearl_thread); # запустить нить
 
sub pearl_thread {      # эта подпрограмма 
   print "Это нить.\n"; # будет выполняться как нить
}                       #

Итак, в определенной точке программы нить начала выполняться параллельно действиям в основной программе. Куда же должен произойти возврат, когда нить завершится? Это задается в основной программе с помощью метода join, который приостанавливает работу программы до завершения выполнения нити и возвращает результат, вычисленный нитью:

@result = $thread->join;

Действие, выполняемое методом join, называется "присоединение нити" или "объединение потоков". Как это происходит, показано на рис. 16.2.

Присоединение нити с помощью join()

Рис. 16.2. Присоединение нити с помощью join()

Каждой нити присваивается числовой идентификатор (Thread Identifier, TID), который можно получить с помощью метода tid. Создание нескольких нитей, объединение потоков и возврат значений из параллельно выполняющихся подпрограмм можно показать на таком примере:

use threads; # подключить многопоточные средства
my @thread = (); # массив объектов типа threads
for (my $i = 0; $i <= 2; $i++) { # создаем 3 нити
   $thread[$i] = threads->new(\&pearl_thread, $i);
   print "Создана $i-я нить. TID=", $thread[$i]->tid, "\n";
}
for (my $i = 2; $i >= 0; $i--) { # присоединяем нити
   print "$i-я нить вернула ", $thread[$i]->join, "\n";
}
sub pearl_thread ($) {            # нить получает
   my $number = shift;            # число, генерирует 
   my $random = int(rand(7)) + 1; # случайное значение,
   print "\t$number-я нить ждет $random сек.\n";
   sleep $random;                 # и, подождав немного,
   return $random;                # возвращает его
}

Сообщения, выводимые при выполнении этой программы, подтверждают независимое выполнение нитей и основной программы:

Создана 0-я нить. TID=1
Создана 1-я нить. TID=2
        1-я нить ждет 7 сек.
        0-я нить ждет 1 сек.
Создана 2-я нить. TID=3
        2-я нить ждет 3 сек.
2-я нить вернула 3
1-я нить вернула 7
0-я нить вернула 1

Параллельно выполняющийся поток можно "отсоединить", игнорируя его значение: это делается методом $thread->detach, после выполнения которого присоединить нить будет невозможно.

Нити, выполняющиеся параллельно с основной программой, могут иметь доступ к общим переменным: скалярам, массивам и хэшам. Это делается с помощью явного указания для разделяемой переменной атрибута shared. Помеченная этим атрибутом переменная будет доступна для чтения и изменения в параллельном потоке. Для остальных переменных при отсоединении нити создаются локальные для каждого потока копии. Это демонстрируется таким примитивным примером:

use threads;         # подключить многопоточные средства
use threads::shared; # и средства разделения данных 

my $public : shared = 0; # разделяемая переменная
my $private         = 0; # неразделяемая переменная

threads->new(sub {   # нить из анонимной подпрограммы
   $public++; $private++;   # изменяем значения
print "$public $private\n"; # будет выведено: 1 1
})->join;            # дожидаемся результатов выполнения:
  
print "$public ",    # 1 ($public изменена в нити)
      "$private\n";  # 0 (в нити изменена копия $private)

Чтобы предотвратить в одной нити изменение другими нитями значения разделяемой переменной, эту переменную нужно заблокировать при помощи функции lock(). Разблокирование переменной происходит не с помощью функции, а при выходе из блока, в котором она была заблокирована. Это делается таким образом:

{ # блок для работы с разделяемой переменной
   lock $variable;         # заблокировать переменную
   $variable = $new_value; # и изменить ее значение
} # здесь $variable автоматически разблокируется

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

use threads;       # подключить средства
use Thread::Queue; # и модуль работы с очередью

my $data_queue = Thread::Queue->new; # создаем очередь
my $thread = threads->new(\&reader); # и нить
# помещаем в очередь скалярные данные:
$data_queue->enqueue(1987);               # число
$data_queue->enqueue('год');              # строку
$data_queue->enqueue('рождения', 'Perl'); # список
$thread->join; # ожидаем окончания нити 
exit;          # перед завершением программы

sub reader {   # извлекаем данные из очереди,
   while ($data_queue->pending) { # пока она не пуста
      my $data_element = $data_queue->dequeue;
      print "'$data_element' извлечен из очереди\n";
   } 
}

Автоматическая синхронизация доступа к очереди гарантирует очередность записи в очередь и чтение из нее, что видно из выполнения этого примера:

'1987' извлечен из очереди
'год' извлечен из очереди
'рождения' извлечен из очереди
'Perl' извлечен из очереди

Конечно, имеющиеся в Perl средства работы с легковесными процессами не ограничиваются перечисленными выше. Стандартные модули предоставляют и другие возможности эффективно организовать различные алгоритмы многопоточной обработки, не говоря уже о дополнительных модулях, имеющихся в архивах CPAN.

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

< Лекция 15 || Лекция 16: 1234 || Лекция 17 >
Сергей Крупко
Сергей Крупко

Добрый день.

Я сейчас прохожу курс  повышения квалификации  - "Профессиональное веб-программирование". Мне нужно получить диплом по этому курсу. Я так полагаю нужно его оплатить чтобы получить диплом о повышении квалификации. Как мне оплатить этот курс?

 

Галина Башкирова
Галина Башкирова

Здравствуйте, недавно закончила курс по проф веб программиованию, мне прислали методические указания с примерами тем, однако темы там для специальности 

Системный администратор информационно-коммуникационных» систем.
Мне нужно самой найти тему? или делать по высланным темам