Выпуск 26. Апрель 2015

Работа с WebSocket в Perl | Содержание | Грамматики в Perl 6

Промисы в Perl 6

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

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

Базовые возможности

Объект создается вызововом Promise.new, а статус обещания доступен в методе status. Пока никаких действий не выполнено, промис находится в состоянии Planned:

my $p = Promise.new;
say $p.status; # Planned

Метод keep переводит обещание в статус сдержанного (Kept):

$p.keep;
say $p.status; # Kept

Аналогично, обещание можно нарушить, вызвав метод break:

my $p1 = Promise.new;
say $p1.status; # Planned

$p1.break;
say $p1.status; # Broken

Вместо вызова метода status возможно преобразовать объект типа Promise в булеву величину, вызвав метод Bool или воспользовавшись унарным оператором ?:

say $p.Bool;
say ?$p;

Обратите внимание, что в этом случае возвращается только статус обещания (то есть либо еще не известно, сдержано оно или нарушено, либо уже известно), но не его результат.

Результат сообщает метод result, но с ним нужно быть осторожным. Если обещание сдержано, result возвращает истину:

my $p = Promise.new;
$p.keep;
say $p.result; # True

Обращение к result блокирует программу до тех пор, пока обещание не выйдет из статуса Planned.

Если обещание нарушено:

my $p = Promise.new;
$p.break;
say $p.result;    

то при вызове метода result возникает исключение:

$ perl6 promise3.pl 
False
  in method result at src/gen/m-CORE.setting:23096
  in block <unit> at promise3.pl:3

Исключения возможно избежать, спрятав вызов внутрь блока try (но say тоже ничего не напечатает):

my $p = Promise.new;
$p.break;
try {
    say $p.result;
}

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

При вызове keep или break возможно передать параметр с сообщением, которое может быть или текстом, или объектом. В этом случае при вызове result вместо True и False (внутри исключения) будет возвращено переданное сообщение.

Фабричные методы

В классе Promise определены несколько интересных методов-фабрик, создающих промисы.

start

Метод start создает промис, внутри которого выполняется блок кода. Вместо явного вызова метода на классе Promise.start удобнее воспользоваться одноименным предопределенным ключевым словом:

my $p = start {
    42
}

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

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

my $p = start {
    42
}
say $p.result; # 42
say $p.status; # Kept

Важно понимать, что создание блока start еще не означает, что код из него выполнен. Метод start сразу возвращает управление, поэтому если тут же попытаться узнать статус промиса, то результат может оказаться неверным. В предыдущем примере вызов $p.result блокировал выполнение программы до того момента, когда код из блока полностью выполнится, соответственно, результат будет содержать сообщение, которое вернул блок, а статус изменится в состояние Kept.

Если поменять местами последние две строки, результат может оказаться другим. Для более воспроизводимого результата можно добавить паузу внутри блока:

my $p = start {
    sleep 1;
    42
}
say $p.status; # Planned
say $p.result; # 42
say $p.status; # Kept

Первый вызов $p.status происходит сразу после создания блока с промисом, и поэтому он еще не выполнен, так что статус оказывается Planned. А второй вывзов происходит уже после того, как метод $p.result дождался выполнения блока кода.

Если же блок кода вызвал исключение, то промис окажется нарушенным:

my $p = start {
    die;
}
try {
    say $p.result;
}
say $p.status; # Эта строка выполнится и напечатает Broken

Вторая ловушка с блоком start — важно понимать, что именно вызывает исключение. Например, попытка деления на ноль приведет к исключению только в том случае, когда случится попытка воспользоваться результатом (это называется soft failure), а до тех пор Perl 6 удовлетворится тем, что результат деления на ноль — значение типа Rat.

# $p1 будет Kept
my $p1 = start {
    my $inf = 1 / 0;
}

# $p2 окажется Broken
my $p2 = start {
    my $inf = 1 / 0;
    say $inf;
}

in и at

Методы Promise.in и Promise.at создают промисы, которые будут сдержаны через заданное число секунд или к указанному времени.

Например:

my $p = Promise.in(3);

for 1..5 {
    say $p.status;
    sleep 1;
}

Эта программа напечатает следующее (то есть промис оказывается выполненным через три секунды):

Planned
Planned
Planned
Kept
Kept

anyof и allof

Методы Promise.anyof и Promise.allof создают новые промисы, которые будут сдержаны только тогда, когда будет сдержан хотя бы один из (для anyof) или все (для allof) промисы, указанные при создании.

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

my $code = start {
    sleep 5
}
my $timeout = Promise.in(3);

my $done = Promise.anyof($code, $timeout);
say $done.result;

Этот код в теории должен завершиться после истечения таймаута в три секунды: в этот момент промис $timeout окажется выполненным, поэтому выполненным сразу же станет и промис $done.

В Rakudo Start 2015.03 этот код работает не как ожидается (он ждет пять секунд и сообщает о том, что $code выполнен), и на IRC-канале предложили обходное решение вместо Promise.in(3):

my $timeout = start {
    sleep 3
}

then

Метод then, вызванный на уже определенном промисе, создает еще один, код которого будет вызван после того, как исходный промис будет сдержан или нарушен.

my $p = Promise.in(2);
my $t = $p.then({say "OK"}); # Напечатается через две секунды
say "promissed"; # Печатается сразу
sleep 3;
say "done";

Другой пример, в котором обещание сдержать не удалось:

Promise.start({  # Новый промис.
    say 1 / 0    # Исключение.
}).then({        # Код, выполняемый после поломки.
    say "oops"
}).result        # Необходимо, чтобы дождаться выполнения.

Пример

В заключение приведу пример реализации сотрировки типа sleep sort на промисах.

my @promises;
for @*ARGS -> $a {
    @promises.push(start {
        sleep $a;
        say $a;
    })
}

await(|@promises);

Программа принимает значения с командной строки:

$ perl6 sleep-sort.pl 3 7 4 9 1 6 2 5

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

Продолжение следует

В следующих выпусках журнала я хочу рассказать о других возможностях Perl 6 для параллельной обработки. А пока предлагаю посмотреть доклад Патрика Мишо Parallelism in Perl 6.

Андрей Шитов


Работа с WebSocket в Perl | Содержание | Грамматики в Perl 6
Нас уже 1393. Больше подписчиков — лучше выпуски!

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