Spec-Zone.ru › Perl 5.34

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, Green-потоки 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()" в потоках и "КОНТЕКСТ ПОТОКА" в потоках для получения дополнительной информации о контексте потока и значениях возврата.

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

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.

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

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 присваивает уникальный идентификатор (TID) каждому созданному потоку в вашей программе, присваивая первому созданному потоку TID 1 и увеличивая TID на 1 для каждого нового созданного потока. При использовании как метода класса, threads->tid() может использоваться потоком для получения собственного 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 strict;
 5 use warnings;
 6
 7 use threads;
 8 use Thread::Queue;
 9
10 sub check_num {
11     my ($upstream, $cur_prime) = @_;
12     my $kid;
13     my $downstream = Thread::Queue->new();
14     while (my $num = $upstream->dequeue()) {
15         next unless ($num % $cur_prime);
16         if ($kid) {
17             $downstream->enqueue($num);
18         } else {
19             print("Found prime: $num\n");
20             $kid = threads->create(\&check_num, $downstream, $num);
21             if (! $kid) {
22                 warn("Sorry.  Ran out of threads.\n");
23                 last;
24             }
25         }
26     }
27     if ($kid) {
28         $downstream->enqueue(undef);
29         $kid->join();
30     }
31 }
32
33 my $stream = Thread::Queue->new(3..1000, undef);
34 check_num($stream, 2);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Соображения производительности

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

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

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

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

Вопросы часто задаваемые по comp.programming.threads, http://www.serpentine.com/~bos/threads-faq/

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

Артур Бергман, «Где волшебники боятся ступить», 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–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.34.0/perlthrtut

Spec-Zone.ru

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