Spec-Zone.ru › Perl 5.36

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 вы можете создавать дополнительные потоки. Мы покажем вам, как, когда и почему.

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

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

Клиент/сервер

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

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

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

Команда исполнителей

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

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

Конвейер

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

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

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

Какой тип потоков используют 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, чтобы повысить производительность в многопоточной среде.

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

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

Основы потоков

Модуль 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 и "CONTEXT ПОТОКА" в 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 и старыми многопоточными системами 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. Часто стандартные методы неудобны и сложны в реализации (например, ожидание условия). Где это возможно, обычно проще использовать 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, видимый другим потокам, изменялся другим потоком между временем, когда он был получен в правой части, и временем хранения нового значения.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Аналогично, смешивание сигналов и потоков может быть проблематичным. Реализации зависят от платформы, и даже семантика POSIX может не быть такой, как вы ожидаете (и Perl даже не предоставляет вам полный API POSIX). Например, нет гарантии, что сигнал, отправленный многопоточной Perl-приложение, будет перехвачен каким-либо определенным потоком. (Однако недавно добавленная функция обеспечивает возможность отправки сигналов между потоками. Более подробную информацию см. в разделе «THREAD SIGNALLING» в документации по потокам.)

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

Вопрос о том, являются ли различные вызовы библиотек потокобезопасными, находится вне контроля 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

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

Вот краткая библиография, любезно предоставленная Юргеном Кристоффелем:

Вводные материалы

Биррелл, Эндрю Д. Введение в программирование с использованием потоков. Digital Equipment Corporation, 1989, Исследовательский отчёт DEC-SRC #35 онлайн как https://www.hpl.hp.com/techreports/Compaq-DEC/SRC-RR-35.pdf (настоятельно рекомендуется)

Робинс, Кей. А., и Стивен Робинс. Практическое программирование Unix: Руководство по совместной работе, связи и многопоточности. Prentice-Hall, 1996.

Луис, Билл и Дэниел Дж. Берг. Многопоточное программирование с использованием Pthreads. Prentice Hall, 1997, ISBN 0-13-443698-9 (хорошее введение в потоки).

Нельсон, Грег (редактор). Системное программирование с Modula-3. Prentice Hall, 1991, ISBN 0-13-590464-1.

Николс, Брэдфорд, Дик Баттлар и Жакелин Проуль Фарелл. Программирование Pthreads. O'Reilly & Associates, 1996, ISBN 156592-115-1 (покрывает POSIX потоки).

Ссылки по ОС

Бойкин, Джозеф, Дэвид Киршен, Алан Лангерман и Сьюзан Ловеро. Программирование под Mach. Addison-Wesley, 1994, ISBN 0-201-52739-1.

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

Сильбершац, Абрахам и Питер Б. Галвин. Концепции операционных систем, 4-е изд. Addison-Wesley, 1995, ISBN 0-201-59292-4

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

Арнольд, Кен и Джеймс Гослинг. Язык программирования Java, 2-е изд. Addison-Wesley, 1998, ISBN 0-201-31006-6.

FAQ comp.programming.threads, http://www.serpentine.com/~bos/threads-faq/

Ле Сержан, Т. и Б. Бертомиу. «Инкрементная многопоточная сборка мусора на архитектурах с практически совместной памятью» в Управлении памятью: Доклады международного семинара IWMM 92, Сен-Мало, Франция, сентябрь 1992 г., Йвес Беккерс и Жак Коэн, ред. Springer, 1992, ISBN 3540-55940-X (реальные применения потоков).

Артур Бергман, «Где волшебники боятся ступить», 11 июня 2002 г., http://www.perl.com/pub/a/2002/06/11/threads.html

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

Благодарим (в произвольном порядке) Хаима Френкеля, Стива Финка, Гурусами Сарати, Илью Захаревича, Бенджамина Сугара, Юргена Кристоффеля, Джошуа Приткина и Алана Берлисона за помощь в проверке и доработке этой статьи. Большое спасибо Тому Кристиансену за переработку генератора простых чисел.

АВТОР

Дан Сугальски <dan@sidhe.org>

Небольшие изменения, внесённые Артуром Бергманом, чтобы соответствовать новой модели/модулю потоков.

Незначительно переработано Йоргом Вальтером <jwalt@cpan.org> для большей конкретики относительно потокобезопасности Perl-кода.

Несколько переделано Элизабет Маттисэн <liz@dijkmat.nl> для меньшего акцента на yield().

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

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

© 1993–2021 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.36.0/perlthrtut

Spec-Zone.ru

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