Выпуск 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 →