Выпуск 24. Февраль 2015

Fuzzing-тестирование perl-интерпретатора с помощью afl | Содержание | Perl 6, или Get ready to party

Каналы в Perl 6

Первая часть обзора возможностей Perl 6 для параллельных и конкуррентных вычислений

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

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

Rakudo 2014.12

После выхода предыдущей статьи появилось обновление Rakudo Star, поэтому прежде чем продолжить, есть смысл обновиться. Процедура установки из нового дистрибутива (rakudo-star-2014.12.1.tar.gz) осталась неизменной. О том, что изменилось в самом Rakudo, можно почитать на сайте проекта.

$ perl6 -v
This is perl6 version 2014.12 built on MoarVM version 2014.12

Каналы

Идея очень простая (и уже реализованная, например, в Go). Создается канал, в который можно писать, и из которого можно читать. Эдакий пайп, но канал.

Запись и чтение

В Perl 6 существует класс Channel, где определены, в частности, методы .send и .receive. Вот простейший пример, в котором в канал $c пишется целое число, которое тут же читается и выводится на экран:

my $c = Channel.new;
$c.send(42);
say $c.receive; # 42

Канал, как и любую переменную, можно передать в функцию, и тогда внутри нее удастся читать из этого канала:

my $ch = Channel.new;
$ch.send(2015);
func($ch);

sub func($ch) {
    say $ch.receive; # 2015
}

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

my $channel = Channel.new;

# В канал уходят несколько нечетных чисел:
for <1 3 5 7 9> {
    $channel.send($_);
}

# А теперь они читаются до тех пор, пока в канале есть данные.
# while @a -> $x эквивалентно for my $x (@a) в Perl 5.
while $channel.poll -> $x {
    say $x;
}

# После того, как все прочитано, возвращается Nil.
$channel.poll.say; # Nil

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

Если же использовать в цикле метод .receive, но закрыть перед этим канал:

$channel.close;
while $channel.receive -> $x {
    say $x;
}

то уже находящиеся в канале данные возможно прочесть, но после того, как они закончатся, произойдет исключение: Cannot receive a message on a closed channel. Код, разумеется, можно поместить внутрь блока try, но куда проще использовать .poll.

$channel.close;
try {
    while $channel.receive -> $x {
        say $x;
    }
}

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

Метод list

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

my $c = Channel.new;

$c.send(5);
$c.send(6);

$c.close;
say $c.list; # 5 6

Метод блокирует программу до тех пор, пока канал не иссякнет, поэтому перед вызовом полезно закрыть канал, вызвав .close.

За пределами скаляров

Здесь уместно удивиться, почему для чтения списков создан отдельный метод вместо того, чтобы изменить работу метода .receive в списочном контексте. А все потому, что в Perl 6 списки и хеши вполне могут быть использованы точно так же, как скаляры: и там, где в Perl 5 список развернулся бы в набор отдельных значений, в Perl 6 он передается как единая переменная. Поэтому возможны такие трюки:

my $c = Channel.new;
my @a = (2, 4, 6, 8);
$c.send(@a);

say $c.receive; # 2 4 6 8

Массив @a передается в поток как единое целое, и точно так же извлекается весь целиком за один вызов .receive;

Более того, если присвоить результат скалярной переменной, то в этом контейнере окажется массив:

my $x = $c.receive;
say $x.WHAT; # (Array)

То же самое будет работать, например, и с хешами:

my $c = Channel.new;
my %h = (alpha => 1, beta => 2);
$c.send(%h);

say $c.receive; # "alpha" => 1, "beta" => 2

Вместо метода .list возможно использовать сам канал в списочном контексте (предварительно закрыв канал):

$c.close;
my @v = @$c;
say @v;

Или так:

$c.close;
for @$c -> $x {
    say $x;
}

Метод .closed

В классе Channel определен еще один полезный метод .closed, позволяющий проверить, открыт канал или нет:

my $c = Channel.new;
say "open" if !$c.closed; # открыт

$c.close;
say "closed" if $c.closed; # закрыт

Несмотря на простоту использования метода, на самом деле он возвращает не булево значение, а объект-промис (переменную типа Promise, о них в следующий раз). В первом случае обещание (промис) того, что канал закрыт, еще только дано:

Promise.new(scheduler => ThreadPoolScheduler.new(initial_threads => 0,
max_threads => 16, uncaught_handler => Callable), status => PromiseStatus::Planned)

А во втором оно уже сдержано: канал к этому времени действительно закрыт.

Promise.new(scheduler => ThreadPoolScheduler.new(initial_threads => 0,
max_threads => 16, uncaught_handler => Callable), status => PromiseStatus::Kept)

Состояние промиса указано в поле status.

Заключение

В этой статье рассказано, как использовать каналы в обычной программе. Но тем не менее каналы считаются thread-safe, поэтому замечательно подходят для многопоточных приложений. Я постараюсь вернуться к этому вопросу в будущем, после рассмотрения промисов и саплаеров.

Андрей Шитов


Fuzzing-тестирование perl-интерпретатора с помощью afl | Содержание | Perl 6, или Get ready to party
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье