Spec-Zone.ru › Perl 5.38

perlthrtut

СОДЕРЖАНИЕ

  • ИМЯ
  • ОПИСАНИЕ
  • Что такое поток?
  • Модели многопоточных программ
    • Босс/работник
    • Бригада рабочих
    • Конвейер
  • Какой тип потоков используют Perl-нити?
  • Модули, безопасные для потоков
  • Основы потоков
    • Базовая поддержка потоков
    • Примечание о примерах
    • Создание потоков
    • Ожидание завершения потока
    • Игнорирование потока
    • Завершение процесса и потока
  • Потоки и данные
    • Общие и не общие данные
    • Опасности с потоками: гонки
  • Синхронизация и управление
    • Управление доступом: lock()
    • Опасность с потоками: тупики
    • Очереди: передача данных
    • Семафоры: синхронизация доступа к данным
    • Базовые семафоры
    • Расширенные семафоры
    • Ожидание условия
    • Отказ от управления
  • Общие утилиты для работы с потоками
    • В каком потоке я нахожусь?
    • Идентификаторы потоков
    • Это одни и те же потоки?
    • Какие потоки работают?
  • Полный пример
  • Различные реализации потоков
  • Соображения по производительности
  • Изменения области процесса
  • Безопасность системных библиотек для потоков
  • Заключение
  • СМОТРИТЕ ТАКЖЕ
  • Библиография
    • Вводные тексты
    • Ссылки по ОС
    • Другие ссылки
  • Благодарности
  • АВТОР
  • Авторские права

ИМЯ

perlthrtut - Руководство по потокам в Perl

ОПИСАНИЕ

Это руководство описывает использование потоков интерпретатора Perl (иногда называемых ithreads). В этой модели каждый поток выполняется в собственном интерпретаторе Perl, и любой обмен данными между потоками должен быть явным. Пользовательский интерфейс для ithreads использует класс threads.

ПРИМЕЧАНИЕ: Раньше существовал другой тип потоков Perl, называемый моделью 5.005, которая использовала класс threads. Эта старая модель имела проблемы, устарела и была удалена в версии 5.10. Настоятельно рекомендуется как можно скорее переместить любой существующий код потоков 5.005 на новую модель.

Вы можете определить, какой (или ни один) тип потоков у вас есть, запустив perl -V и посмотрев раздел Platform. Если у вас есть useithreads=define, у вас есть ithreads, если у вас есть use5005threads=define, у вас есть потоки 5.005. Если у вас нет ни того ни другого, у вас нет встроенной поддержки потоков. Если у вас есть оба, у вас проблемы.

Модули threads и threads::shared включены в дистрибутив Perl. Кроме того, они поддерживаются как отдельные модули на CPAN, поэтому вы можете проверить их на наличие обновлений.

Что такое поток?

Поток — это поток управления в программе с одной точкой выполнения.

Звучит очень похоже на процесс, не так ли? Ну, так оно и есть. Потоки являются одной из частей процесса. Каждый процесс имеет по крайней мере один поток, и до сих пор каждый процесс, выполняющий Perl, имел только один поток. Однако начиная с версии 5.8, вы можете создавать дополнительные потоки. Мы покажем вам, как, когда и почему.

Модели многопоточных программ

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

Босс/работник

Модель «босс/работник» обычно имеет один поток «босс» и один или несколько потоков «работник». Поток «босс» собирает или генерирует задачи, которые нужно выполнить, а затем распределяет эти задачи соответствующему потоку «работник».

Эта модель распространена в графических интерфейсах пользователя и серверных программах, где основной поток ожидает какого-либо события, а затем передает это событие соответствующим потокам «работник» для обработки. После того, как событие передано, основной поток возвращается к ожиданию другого события.

Поток «босс» выполняет относительно небольшую работу. Хотя задачи, возможно, не выполняются быстрее, чем с помощью любого другого метода, он, как правило, обеспечивает наилучшие времена отклика для пользователя.

Бригада рабочих

В модели «бригада рабочих» создается несколько потоков, выполняющих по существу одно и то же действие над разными частями данных. Она тесно отражает классическую параллельную обработку и векторные процессоры, где большое количество процессоров выполняют одно и то же действие над многими частями данных.

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

Конвейер

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

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

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

Какой тип потоков используют Perl-нити?

Если у вас есть опыт работы с другими реализациями потоков, вы можете обнаружить, что вещи не совсем такие, как вы ожидаете. Очень важно помнить, когда работаете с потоками Perl, что потоки Perl не являются потоками X для всех значений X. Это не потоки POSIX, DecThreads, зелёные потоки Java или потоки Win32. Есть сходства, и основные понятия одинаковые, но если вы начнете искать детали реализации, вас ожидает разочарование или путаница. Возможно, и то, и другое.

Это не означает, что потоки Perl полностью отличаются от всего, что было до этого. Нет. Модель потоков Perl во многом обязана другим моделям потоков, особенно POSIX. Так же как Perl не является C, так и потоки Perl не являются потоками POSIX. Поэтому, если вы ищете мьютексы или приоритеты потоков, вам следует немного отступить и подумать о том, что вы хотите сделать, и как это можно сделать с помощью Perl.

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

Потоки Perl отличаются.

Модули, безопасные для потоков

Добавление потоков существенно изменило внутреннюю работу Perl. Это имеет последствия для людей, которые пишут модули с кодом XS или внешними библиотеками. Однако, поскольку данные Perl по умолчанию не обмениваются между потоками, модули Perl имеют высокие шансы быть безопасными для потоков или могут быть легко сделаны безопасными для потоков. Модули, не помеченные как безопасные для потоков, должны быть протестированы или проверены перед использованием в коде производства.

Не все модули, которые вы можете использовать, являются безопасными для потоков, и вы всегда должны предполагать, что модуль небезопасен, если документация не указывает обратное. Это относится к модулям, которые распространяются в составе ядра. Потоки — относительно новая функция, и даже некоторые стандартные модули не являются безопасными для потоков.

Даже если модуль безопасен для потоков, это не означает, что модуль оптимизирован для эффективной работы с потоками. Модуль, возможно, можно переписать, чтобы использовать новые возможности потокового Perl для повышения производительности в многопоточной среде.

Если по какой-то причине вы используете модуль, не безопасный для потоков, вы можете защитить себя, используя его только из одного и только одного потока. Если вам необходимо получить доступ к такому модулю из нескольких потоков, вы можете использовать семафоры и дисциплину программирования для управления доступом к нему. Семафоры описаны в разделе «Базовые семафоры».

См. также «Безопасность системных библиотек для потоков».

END_OF_DOCUMENT_MARKER

Основные понятия о потоках

Модуль threads предоставляет основные функции, необходимые для написания многопоточных программ. В следующих разделах мы рассмотрим основы, показывая, что нужно сделать для создания многопоточной программы. После этого мы рассмотрим некоторые возможности модуля threads, которые упрощают многопоточное программирование.

Базовая поддержка потоков

Поддержка потоков — это параметр компиляции Perl. Это то, что включается или выключается при построении Perl на вашем сайте, а не при компиляции ваших программ. Если ваш Perl не был скомпилирован с поддержкой потоков, то любая попытка использовать потоки завершится неудачей.

Ваши программы могут использовать модуль Config для проверки, включена ли поддержка потоков. Если ваша программа не может работать без неё, вы можете сделать что-то вроде:

use Config;
$Config{useithreads} or
    die('Recompile Perl with threads to run this program.');

Программа, которая потенциально использует потоки, используя потенциально многопоточный модуль, может иметь код такого рода:

use Config;
use MyMod;

BEGIN {
    if ($Config{useithreads}) {
        # We have threads
        require MyMod_threaded;
        import MyMod_threaded;
    } else {
        require MyMod_unthreaded;
        import MyMod_unthreaded;
    }
}

Поскольку код, который работает как с потоками, так и без них, обычно довольно запутанный, лучше изолировать код, специфичный для потоков, в свой собственный модуль. В нашем примере выше это то, что MyMod_threaded представляет собой, и оно импортируется только если мы работаем в многопоточном Perl.

Примечание по примерам

В реальной ситуации следует позаботиться о том, чтобы все потоки завершили выполнение перед завершением программы. В этих примерах для простоты такой заботы не продемонстрировано. Запуск этих примеров как есть выведет сообщения об ошибках, обычно вызванные тем, что потоки всё ещё работают при завершении программы. Это не должно вызывать беспокойства.

Создание потоков

Модуль threads предоставляет инструменты, необходимые для создания новых потоков. Как и любой другой модуль, вы должны сообщить Perl, что хотите его использовать; use threads; импортирует все необходимые части для создания базовых потоков.

Самый простой и прямой способ создания потока — с помощью create():

use threads;

my $thr = threads->create(\&sub1);

sub sub1 {
    print("In the thread\n");
}

Метод create() принимает ссылку на подпрограмму и создаёт новый поток, который начинает выполнение в указанной подпрограмме. Управление передаётся как подпрограмме, так и вызывающей стороне.

Если необходимо, ваша программа может передавать параметры в подпрограмму как часть запуска потока. Просто включите список параметров в вызов threads->create() так:

use threads;

my $Param3 = 'foo';
my $thr1 = threads->create(\&sub1, 'Param 1', 'Param 2', $Param3);
my @ParamList = (42, 'Hello', 3.14);
my $thr2 = threads->create(\&sub1, @ParamList);
my $thr3 = threads->create(\&sub1, qw(Param1 Param2 Param3));

sub sub1 {
    my @InboundParameters = @_;
    print("In the thread\n");
    print('Got parameters >', join('<>',@InboundParameters), "<\n");
}

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

new() — это синоним для create().

Ожидание завершения потока

Поскольку потоки также являются подпрограммами, они могут возвращать значения. Чтобы дождаться завершения потока и извлечь любые возвращаемые им значения, можно использовать метод join():

use threads;

my ($thr) = threads->create(\&sub1);

my @ReturnData = $thr->join();
print('Thread returned ', join(', ', @ReturnData), "\n");

sub sub1 { return ('Fifty-six', 'foo', 2); }

В примере выше метод join() возвращает результат, как только поток завершается. Кроме ожидания завершения потока и сбора любых возвращаемых им значений, join() также выполняет необходимые системные операции по завершению для потока. Это может быть важно, особенно для долгоживущих программ, запускающих много потоков. Если вам не нужны возвращаемые значения и вы не хотите ждать завершения потока, вы должны вызвать метод detach() вместо этого, как описано ниже.

ПРИМЕЧАНИЕ: В примере выше поток возвращает список, поэтому вызов создания потока должен быть сделан в контексте списка (т. е., my ($thr)). См. "$thr->join()" в threads и "КОНТЕКСТ ПОТОКА" в threads для получения более подробной информации о контексте потока и возвращаемых значениях.

Игнорирование потока

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

В этом случае используется метод detach(). После отсоединения потока он будет выполняться до завершения; затем Perl автоматически очистит его.

use threads;

my $thr = threads->create(\&sub1);   # Spawn the thread

$thr->detach();   # Now we officially don't care any more

sleep(15);        # Let thread run for awhile

sub sub1 {
    my $count = 0;
    while (1) {
        $count++;
        print("\$count is $count\n");
        sleep(1);
    }
}

После того как поток отсоединён, его нельзя объединить, и любые возвращаемые данные, которые он мог произвести (если он был завершён и ожидал объединения), теряются.

detach() также можно вызывать как метод класса, чтобы позволить потоку отсоединиться:

use threads;

my $thr = threads->create(\&sub1);

sub sub1 {
    threads->detach();
    # Do more work
}

Завершение процесса и потока

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

Действие, которое завершает процесс, завершит все запущенные потоки. die() и exit() обладают этим свойством, и Perl завершается, когда завершается основной поток, возможно, неявно, если программа завершается без явного указания, даже если этого не требуется.

В качестве примера этого случая, этот код выводит сообщение "Perl завершился с активными потоками: 2 активных и не объединённых":

use threads;
my $thr1 = threads->new(\&thrsub, "test1");
my $thr2 = threads->new(\&thrsub, "test2");
sub thrsub {
   my ($message) = @_;
   sleep 1;
   print "thread $message\n";
}

Но когда к концу добавляются следующие строки:

$thr1->join();
$thr2->join();

он выводит две строки вывода, что, возможно, более полезный результат.

Потоки и данные

Теперь, когда мы разобрались с основами потоков, настало время перейти к следующей теме: данные. Потоки вводят несколько осложнений в доступ к данным, о которых программы без потоков никогда не беспокоятся.

Общие и частные данные

Наиболее существенное различие между Perl ithreads и старой моделью потоков 5.005, или, вообще, большинством других систем потоков, состоит в том, что по умолчанию данные не разделяются. При создании нового потока Perl все данные, связанные с текущим потоком, копируются в новый поток и являются частными для этого нового потока! Это похоже на то, что происходит при разветвлении Unix-процесса, за исключением того, что в этом случае данные просто копируются в другую область памяти в рамках одного процесса, а не происходит фактического разветвления.

Однако для использования потоков обычно нужно, чтобы потоки делили хотя бы некоторые данные между собой. Это делается с помощью модуля threads::shared и атрибута :shared:

use threads;
use threads::shared;

my $foo :shared = 1;
my $bar = 1;
threads->create(sub { $foo++; $bar++; })->join();

print("$foo\n");  # Prints 2 since $foo is shared
print("$bar\n");  # Prints 1 since $bar is not shared

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

use threads;
use threads::shared;

my $var          = 1;
my $svar :shared = 2;
my %hash :shared;

... create some threads ...

$hash{a} = 1;       # All threads see exists($hash{a})
                    # and $hash{a} == 1
$hash{a} = $var;    # okay - copy-by-value: same effect as previous
$hash{a} = $svar;   # okay - copy-by-value: same effect as previous
$hash{a} = \$svar;  # okay - a reference to a shared variable
$hash{a} = \$var;   # This will die
delete($hash{a});   # okay - all threads will see !exists($hash{a})

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

Ловушки потоков: гонки

Хотя потоки привносят новые полезные инструменты, они также привносят ряд ловушек. Одна из ловушек — гонка:

use threads;
use threads::shared;

my $x :shared = 1;
my $thr1 = threads->create(\&sub1);
my $thr2 = threads->create(\&sub2);

$thr1->join();
$thr2->join();
print("$x\n");

sub sub1 { my $foo = $x; $x = $foo + 1; }
sub sub2 { my $bar = $x; $x = $bar + 1; }

Что вы думаете, будет $x? К сожалению, ответ — это зависит. И sub1() и sub2() обращаются к глобальной переменной $x, один раз для чтения и один раз для записи. В зависимости от факторов, начиная от алгоритма планирования вашей реализации потоков и заканчивая фазой луны, $x может быть 2 или 3.

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

use threads;
my $x :shared = 2;
my $y :shared;
my $z :shared;
my $thr1 = threads->create(sub { $y = $x; $x = $y + 1; });
my $thr2 = threads->create(sub { $z = $x; $x = $z + 1; });
$thr1->join();
$thr2->join();

Два потока оба обращаются к $x. Каждый поток может быть прерван в любой момент или выполняться в любом порядке. В итоге $x может быть 3 или 4, а $y и $z могут быть 2 или 3.

Даже $x += 5 или $x++ не гарантируют атомарность.

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

Синхронизация и управление

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

Управление доступом: lock()

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

use threads;
use threads::shared;

my $total :shared = 0;

sub calc {
    while (1) {
        my $result;
        # (... do some calculations and set $result ...)
        {
            lock($total);  # Block until we obtain the lock
            $total += $result;
        } # Lock implicitly released at end of scope
        last if $result == 0;
    }
}

my $thr1 = threads->create(\&calc);
my $thr2 = threads->create(\&calc);
my $thr3 = threads->create(\&calc);
$thr1->join();
$thr2->join();
$thr3->join();
print("total=$total\n");

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

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

Вы можете блокировать массивы и хеш-таблицы, а также скаляры. Однако блокировка массива не будет блокировать последующие блокировки элементов массива, только блокирует попытки блокировки самого массива.

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

my $x :shared;
doit();

sub doit {
    {
        {
            lock($x); # Wait for lock
            lock($x); # NOOP - we already have the lock
            {
                lock($x); # NOOP
                {
                    lock($x); # NOOP
                    lockit_some_more();
                }
            }
        } # *** Implicit unlock here ***
    }
}

sub lockit_some_more {
    lock($x); # NOOP
} # Nothing happens here

Обратите внимание, что нет функции unlock() — единственный способ разблокировать переменную — это выйти из области её видимости.

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

Ловушка потоков: тупик

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

use threads;

my $x :shared = 4;
my $y :shared = 'foo';
my $thr1 = threads->create(sub {
    lock($x);
    sleep(20);
    lock($y);
});
my $thr2 = threads->create(sub {
    lock($y);
    sleep(20);
    lock($x);
});

Эта программа, вероятно, зависнет, пока вы её не прервёте. Единственный способ избежать зависания — если одна из двух нитей получит оба замка первыми. Гарантированно зависающая версия сложнее, но принцип тот же.

Первая нить захватывает замок на $x, затем, после паузы, в течение которой вторая нить, вероятно, успела выполнить некоторую работу, пытается захватить замок на $y. Тем временем вторая нить захватывает замок на $y, а затем пытается захватить замок на $x. Вторая попытка захвата замка для обеих нитей будет заблокирована, каждая из них ожидая, пока другая освободит свой замок.

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

Существует несколько способов решения подобной проблемы. Лучший способ — всегда заставлять все нити захватывать замки в точно таком же порядке. Если, например, вы блокируете переменные $x, $y, и $z, всегда блокируйте $x перед $y, и $y перед $z. Также желательно удерживать замки как можно меньше времени, чтобы минимизировать риски тупика.

Другие описанные ниже примитивы синхронизации могут страдать от подобных проблем.

Очереди: Передача данных

Очередь — специальный потокобезопасный объект, позволяющий помещать данные в один конец и извлекать их из другого, не беспокоясь о проблемах синхронизации. Они довольно просты и выглядят так:

use threads;
use Thread::Queue;

my $DataQueue = Thread::Queue->new();
my $thr = threads->create(sub {
    while (my $DataElement = $DataQueue->dequeue()) {
        print("Popped $DataElement off the queue\n");
    }
});

$DataQueue->enqueue(12);
$DataQueue->enqueue("A", "B", "C");
sleep(10);
$DataQueue->enqueue(undef);
$thr->join();

Вы создаёте очередь с помощью Thread::Queue->new(). Затем вы можете добавлять списки скаляров в конец с помощью enqueue(), и извлекать скаляры из начала с помощью dequeue(). Очередь не имеет фиксированного размера и может расти по мере необходимости, чтобы вместить все добавленные в неё данные.

Если очередь пуста, dequeue() блокируется, пока другая нить не добавит в неё что-то. Это делает очереди идеальными для циклов событий и других коммуникаций между нитями.

Семафоры: Синхронизация доступа к данным

Семафоры — это своего рода универсальный механизм блокировки. В своей самой простой форме они ведут себя очень похоже на блокируемые скаляры, за исключением того, что они не могут хранить данные и должны быть явным образом разблокированы. В своей продвинутой форме они действуют как своего рода счётчик и могут позволять нескольким нитям иметь замок в любой момент времени.

Базовые семафоры

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

use threads;
use Thread::Semaphore;

my $semaphore = Thread::Semaphore->new();
my $GlobalVariable :shared = 0;

$thr1 = threads->create(\&sample_sub, 1);
$thr2 = threads->create(\&sample_sub, 2);
$thr3 = threads->create(\&sample_sub, 3);

sub sample_sub {
    my $SubNumber = shift(@_);
    my $TryCount = 10;
    my $LocalCopy;
    sleep(1);
    while ($TryCount--) {
        $semaphore->down();
        $LocalCopy = $GlobalVariable;
        print("$TryCount tries left for sub $SubNumber "
             ."(\$GlobalVariable is $GlobalVariable)\n");
        sleep(2);
        $LocalCopy++;
        $GlobalVariable = $LocalCopy;
        $semaphore->up();
    }
}

$thr1->join();
$thr2->join();
$thr3->join();

Все три вызова подпрограммы работают синхронно. Однако семафор гарантирует, что только одна нить имеет доступ к глобальной переменной одновременно.

Расширенные семафоры

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

Каждый семафор имеет прикреплённый к нему счётчик. По умолчанию семафоры создаются со счётчиком, установленным в единицу, down() уменьшает счётчик на единицу, а up() увеличивает его на единицу. Однако мы можем переопределить любое или все из этих значений по умолчанию, просто передав другие значения:

use threads;
use Thread::Semaphore;

my $semaphore = Thread::Semaphore->new(5);
                # Creates a semaphore with the counter set to five

my $thr1 = threads->create(\&sub1);
my $thr2 = threads->create(\&sub1);

sub sub1 {
    $semaphore->down(5); # Decrements the counter by five
    # Do stuff here
    $semaphore->up(5); # Increment the counter by five
}

$thr1->detach();
$thr2->detach();

Если down() пытается уменьшить счётчик ниже нуля, он блокируется, пока счётчик не станет достаточно большим. Обратите внимание, что, хотя семафор может быть создан с начальным значением счётчика равным нулю, любой up() или down() всегда изменяет счётчик как минимум на единицу, поэтому $semaphore->down(0) эквивалентно $semaphore->down(1).

Вопрос, конечно, в том, зачем делать что-то подобное? Зачем создавать семафор с начальным значением счётчика, отличным от единицы, или зачем уменьшать или увеличивать его на значение больше единицы? Ответ — доступность ресурсов. Многие ресурсы, для доступа к которым требуется управление, могут безопасно использоваться более чем одной нитью одновременно.

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

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

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

Ожидание условия

Функции cond_wait() и cond_signal() могут использоваться совместно с замками для уведомления сотрудничающих нитей о том, что ресурс стал доступным. Они очень похожи по использованию на функции из pthreads. Однако для большинства целей очереди проще в использовании и интуитивнее. Подробнее см. threads::shared.

Отказ от управления

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

Пакет потоков Perl предоставляет функцию yield(), которая делает это. yield() довольно проста и работает следующим образом:

use threads;

sub loop {
    my $thread = shift;
    my $foo = 50;
    while($foo--) { print("In thread $thread\n"); }
    threads->yield();
    $foo = 50;
    while($foo--) { print("In thread $thread\n"); }
}

my $thr1 = threads->create(\&loop, 'first');
my $thr2 = threads->create(\&loop, 'second');
my $thr3 = threads->create(\&loop, 'third');

Важно помнить, что yield() — это лишь подсказка для отказа от процессора, фактические действия зависят от вашего оборудования, ОС и библиотек потоков. На многих операционных системах yield() — это пустая операция. Поэтому важно отметить, что не следует строить планирование потоков вокруг вызовов yield(). Это может работать на вашей платформе, но не на других.

Общие вспомогательные процедуры для потоков

Мы рассмотрели основные части пакета потоков Perl, и с этими инструментами вы должны быть готовы к написанию кода и пакетов с потоками. Есть несколько полезных небольших компонентов, которые не очень хорошо вписывались никуда.

В каком потоке я нахожусь?

Метод класса threads->self() предоставляет вашей программе способ получить объект, представляющий поток, в котором она находится в данный момент. Вы можете использовать этот объект так же, как и те, что возвращаются при создании потоков.

Идентификаторы потоков

tid() — это метод объекта потока, который возвращает идентификатор потока потока, который представляет объект. Идентификаторы потоков — это целые числа, основной поток в программе — 0. В настоящее время Perl присваивает уникальный идентификатор каждой нити, созданной в вашей программе, присваивая первой созданной нити идентификатор 1 и увеличивая идентификатор на 1 для каждой новой нити, созданной. Когда используется как метод класса, threads->tid() позволяет нити получить свой собственный идентификатор.

Это одни и те же потоки?

Метод equal() принимает два объекта потока и возвращает true, если объекты представляют один и тот же поток, и false — если нет.

Объекты потоков также имеют перегруженное сравнение ==, чтобы вы могли сравнивать их так же, как и обычные объекты.

Какие потоки работают?

threads->list() возвращает список объектов потоков, по одному для каждого потока, который в настоящее время выполняется и не отсоединён. Полезно для многих задач, включая очистку в конце вашей программы (из основного потока Perl, конечно):

# Loop through all the threads
foreach my $thr (threads->list()) {
    $thr->join();
}

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

ПРИМЕЧАНИЕ: основной поток Perl (поток 0) находится в отсоединённом состоянии и поэтому не отображается в списке, возвращаемом threads->list().

Полный пример

Сбились с толку? Время для демонстрационной программы, показывающей некоторые из изученных вещей. Эта программа находит простые числа с использованием потоков.

 1 #!/usr/bin/perl
 2 # prime-pthread, courtesy of Tom Christiansen
 3
 4 use v5.36;
 5
 6 use threads;
 7 use Thread::Queue;
 8
 9 sub check_num ($upstream, $cur_prime) {
10     my $kid;
11     my $downstream = Thread::Queue->new();
12     while (my $num = $upstream->dequeue()) {
13         next unless ($num % $cur_prime);
14         if ($kid) {
15             $downstream->enqueue($num);
16         } else {
17             print("Found prime: $num\n");
18             $kid = threads->create(\&check_num, $downstream, $num);
19             if (! $kid) {
20                 warn("Sorry.  Ran out of threads.\n");
21                 last;
22             }
23         }
24     }
25     if ($kid) {
26         $downstream->enqueue(undef);
27         $kid->join();
28     }
29 }
30
31 my $stream = Thread::Queue->new(3..1000, undef);
32 check_num($stream, 2);

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

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

Основная работа выполняется подпрограммой check_num(), которая принимает ссылку на входную очередь и простое число, за которое она отвечает. Мы создаём новую очередь (строка 11) и зарезервируем скаляр для потока, который мы, вероятно, создадим позже (строка 10).

Цикл while со строки 12 по строку 24 извлекает скаляр из входной очереди и проверяет его на простое число, за которое отвечает этот поток. Строка 13 проверяет, есть ли остаток при делении проверяемого числа на наше простое число. Если есть, число не делится нацело на наше простое число, поэтому нам нужно либо передать его следующему потоку, если мы его создали (строка 15), либо создать новый поток, если мы его ещё не создали.

Создание нового потока — это строка 18. Мы передаём ему ссылку на созданную очередь и простое число, которое мы нашли. В строках 19–22 мы проверяем, был ли создан наш новый поток, и если нет, прекращаем проверку оставшихся чисел в очереди.

Наконец, после завершения цикла (потому что мы получили 0 или undef в очереди, что служит сигналом для завершения), мы передаём уведомление нашему дочернему потоку и ждём его завершения, если мы создали дочерний поток (строки 25 и 28).

Тем временем, в основном потоке, мы сначала создаём очередь (строка 31) и добавляем в неё все числа от 3 до 1000 для проверки, а также сигнал завершения. Затем нам нужно только передать очередь и первое простое число в подпрограмму check_num() (строка 32).

Вот как это работает. Это довольно просто; как и во многих программах Perl, объяснение намного длиннее, чем сама программа.

Различные реализации потоков

Некоторые сведения о реализациях потоков с точки зрения операционной системы. Существует три основных категории потоков: потоки пользовательского уровня, потоки ядра и потоки ядра многопроцессорных систем.

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

Это самый простой способ реализовать потоки, и так большинство ОС начинают. Большой недостаток заключается в том, что, поскольку ОС ничего не знает о потоках, если один поток заблокируется, заблокируются все. К типичным блокирующим операциям относятся большинство системных вызовов, большинство операций ввода-вывода и такие вещи, как sleep().

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

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

Поскольку ядерные потоки могут прерывать поток в любое время, они выявят некоторые неявные предположения о блокировке, которые вы можете сделать в своей программе. Например, что-то столь же простое, как $x = $x + 2, может вести себя непредсказуемо с ядерными потоками, если $x виден другим потокам, так как другой поток может изменить $x между временем его извлечения в правой части и временем хранения нового значения.

Многопроцессорные ядерные потоки — это заключительный шаг в поддержке потоков. С многопроцессорными ядерными потоками на машине с несколькими процессорами ОС может запланировать два или более потоков для одновременного выполнения на разных процессорах.

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

Помимо различных уровней участия ОС в потоках, разные ОС (и различные реализации потоков для конкретной ОС) по-разному распределяют циклы процессора между потоками.

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

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

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

Большинство современных операционных систем сегодня поддерживают прерывистое многозадачие.

Рассмотрение производительности

Главное, что следует помнить при сравнении ithreads Perl с другими моделями потоков, заключается в том, что для каждого нового созданного потока необходимо сделать полную копию всех переменных и данных родительского потока. Таким образом, создание потоков может быть довольно дорогостоящим, как с точки зрения использования памяти, так и с точки зрения времени, затраченного на создание. Идеальный способ снизить эти затраты — иметь относительно небольшое количество долгоживущих потоков, все созданные достаточно рано (до того, как базовый поток накопил слишком много данных). Конечно, это может быть не всегда возможным, поэтому приходится идти на компромиссы. Однако после создания потока его производительность и дополнительное потребление памяти должны мало отличаться от обычного кода.

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

Изменения области процесса

Обратите внимание, что, хотя сами потоки являются отдельными потоками выполнения, и данные Perl являются потокобезопасными, если они не объявлены явно общими, потоки могут влиять на состояние области процесса, воздействуя на все потоки.

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

Еще более радикальный пример изменения области процесса — chroot(): корневой каталог всех потоков изменяется, и ни один поток не может его отменить (в отличие от chdir()).

Другие примеры изменений области процесса включают umask() и изменение идентификаторов пользователей и групп.

Вы задумываетесь о смешивании fork() и потоков? Пожалуйста, лягте и подождите, пока чувство не пройдет. Имейте в виду, что семантика fork() различается на разных платформах. Например, некоторые системы Unix копируют все текущие потоки в дочерний процесс, в то время как другие копируют только поток, который вызвал fork(). Вас предупредили!

Аналогично, смешивание сигналов и потоков может быть проблематичным. Реализации зависят от платформы, и даже семантика POSIX может не соответствовать вашим ожиданиям (и Perl даже не предоставляет вам полный API POSIX). Например, нет гарантии, что сигнал, отправленный многопоточной программе Perl, будет перехвачен каким-либо конкретным потоком. (Однако недавно добавленная функция предоставляет возможность отправлять сигналы между потоками. Подробнее см. "ОБМЕН СИГНАЛАМИ МЕЖДУ ПОТОКАМИ" в потоках).

Потокобезопасность системных библиотек

Вопрос о том, являются ли различные вызовы библиотек потокобезопасными, находится вне контроля Perl. К вызовам, которые часто не являются потокобезопасными, относятся: localtime(), gmtime(), функции извлечения информации о пользователе, группе и сети (например, getgrent(), gethostent(), getnetent() и т. д.), readdir(), rand(), и srand(). В общем случае, это вызовы, которые зависят от некоторого глобального внешнего состояния.

Если скомпилированный Perl имеет потокобезопасные варианты таких вызовов, они будут использоваться. В остальном Perl зависит от потокобезопасности или небезопасности вызовов. Обратитесь к документации по вызовам вашей библиотеки C.

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

Заключение

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

См. также

Аннотированный POD для threads: https://web.archive.org/web/20171028020148/http://annocpan.org/?mode=search&field=Module&name=threads

Последняя версия threads на CPAN: https://metacpan.org/pod/threads

Аннотированный POD для threads::shared: https://web.archive.org/web/20171028020148/http://annocpan.org/?mode=search&field=Module&name=threads%3A%3Ashared

Последняя версия threads::shared на CPAN: https://metacpan.org/pod/threads::shared

Список рассылки Perl threads: https://lists.perl.org/list/ithreads.html

Библиография

Вот краткая библиография любезно предоставленная Jürgen Christoffel:

Вводные тексты

Birrell, Andrew D. Введение в программирование с потоками. Digital Equipment Corporation, 1989, отчет DEC-SRC Research Report #35 онлайн как https://www.hpl.hp.com/techreports/Compaq-DEC/SRC-RR-35.pdf (высоко рекомендуется)

Robbins, Kay. A., and Steven Robbins. Практическое программирование Unix: Руководство по параллельности, коммуникации и многопоточности. Prentice-Hall, 1996.

Lewis, Bill, and Daniel J. Berg. Программирование с многопоточностью с помощью Pthreads. Prentice Hall, 1997, ISBN 0-13-443698-9 (хорошее введение в потоки).

Nelson, Greg (editor). Программирование систем с Modula-3. Prentice Hall, 1991, ISBN 0-13-590464-1.

Nichols, Bradford, Dick Buttlar, and Jacqueline Proulx Farrell. Программирование с Pthreads. O'Reilly & Associates, 1996, ISBN 156592-115-1 (покрывает потоки POSIX).

Ссылки, относящиеся к ОС

Boykin, Joseph, David Kirschen, Alan Langerman, and Susan LoVerso. Программирование под Mach. Addison-Wesley, 1994, ISBN 0-201-52739-1.

Tanenbaum, Andrew S. Распределенные операционные системы. Prentice Hall, 1995, ISBN 0-13-219908-4 (отличная учебная книга).

Silberschatz, Abraham, and Peter B. Galvin. Концепции операционных систем, 4-е изд. Addison-Wesley, 1995, ISBN 0-201-59292-4

Другие ссылки

Arnold, Ken and James Gosling. Язык программирования Java, 2-е изд. Addison-Wesley, 1998, ISBN 0-201-31006-6.

FAQ по группам новостей comp.programming.threads, http://www.serpentine.com/~bos/threads-faq/

Le Sergent, T. and B. Berthomieu. "Инкрементальное многопоточное сборка мусора на архитектурах с виртуально совмещенной памятью" в Управлении памятью: Докл. международного семинара IWMM 92, Сен-Мало, Франция, сентябрь 1992, Yves Bekkers и Jacques Cohen, ред. Springer, 1992, ISBN 3540-55940-X (приложения потоков в реальной жизни).

Artur Bergman, "Где боятся колдуны", 11 июня 2002 г., http://www.perl.com/pub/a/2002/06/11/threads.html

Благодарности

Благодарим (в произвольном порядке) Chaim Frenkel, Steve Fink, Gurusamy Sarathy, Ilya Zakharevich, Benjamin Sugars, Jürgen Christoffel, Joshua Pritikin и Alan Burlison за помощь в проверке достоверности и доработке этой статьи. Большое спасибо Tom Christiansen за переработку генератора простых чисел.

АВТОР

Dan Sugalski <dan@sidhe.org>

Незначительно изменён Arthur Bergman для соответствия новой модели/модулю потоков.

Несколько переработан Jörg Walter <jwalt@cpan.org> для более точного описания потокобезопасности кода Perl.

Незначительно перестроен Elizabeth Mattijsen <liz@dijkmat.nl> для уменьшения акцента на yield().

Авторские права

Первоначальная версия этой статьи первоначально появилась в The Perl Journal #10 и защищена авторскими правами 1998 The Perl Journal. Она появляется по любезности Jon Orwant и The Perl Journal. Данный документ может быть распространён на тех же условиях, что и Perl сам по себе.

© 1993–2023 Larry Wall and others
Licensed under the GNU General Public License version 1 or later, or the Artistic License.
The Perl logo is a trademark of the Perl Foundation.
https://perldoc.perl.org/5.38.0/perlthrtut

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API