5. Атомарные операции в Perl 6

Внимание: код в этом посте требует Rakudo версии не менее 2017.09.

Как-нибудь мы более подробно поговорим о параллельном программировании в Perl 6, но пока заглянем вперед и создадим простой счетчик, который будет увеличиваться десятью параллельными потоками:

my $c = 0;

await do for 1..10 {
    start {
        $c++ for 1 .. 1_000_000
    }
}

say $c;

Выполнив программу несколько раз, вы удивитесь, насколько разные и насколько неверные результаты она печатает:

$ perl6 atomic-1.pl 
3141187
$ perl6 atomic-1.pl 
3211980
$ perl6 atomic-1.pl 
3174944
$ perl6 atomic-1.pl 
3271573

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

Решение — использование атомарных (atomic) операций. Синтаксис Perl 6 дополнен символом Atom Symbol (U+0x269B) ⚛ (почему-то он именно такого цвета). Вместо $c++ надо написать $c⚛++.

my atomicint $c = 0;

await do for 1..10 {
    start {
        $c⚛++ for 1 .. 1_000_000
    }
}

say $c;

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

$ perl6 atomic-2.pl 
10000000

Именно то, что и требовалось.

Обратите внимание еще на один момент: переменная объявлена как переменная типа atomicint. Это синоним int — машинного целого числа (в отличие от Int, что является классом).

Обычные переменные не могут учавствовать в атомарных операциях; это пресекается компилятором:

$ perl6 -e'my $c; $c⚛++'
Expected a modifiable native int argument for '$target'
  in block  at -e line 1

Атомарность поддерживают еще несколько операторов: префиксные и постфиксные ++ и --, += и -=, а также атомарные операции присваивания = и чтения: ⚛.

Использовать символ ⚛ вовсе необязательно. Существуют альтернативные функции, которые можно вызывать вместо операторов:

my atomicint $c = 1;

my $x = ⚛$c;  $x = atomic-fetch($c);
$c ⚛= $x;     atomic-assign($c, $x);
$c⚛++;        atomic-fetch-inc($c);
$c⚛--;        atomic-fetch-dec($c);
++⚛$c;        atomic-inc-fetch($c);
--⚛$c;        atomic-dec-fetch($c);
$c ⚛+= $x;    atomic-fetch-add($c,$x);

say $x; # 1
say $c; # 3

8 thoughts on “5. Атомарные операции в Perl 6”

      1. Позволь уточнить: т.е. есть задачи, когда подобный разрыв чтения-записи, как в первом примере, допустим. Мне в голову приходит только что-нибудь в духе, когда каждый раз нужно сделать новую запись; пардон, с этой темой я только сегодня столкнулся, поэтому вопрошаю: Я в верном направлении мыслю?

      2. Почему обязательно разрыв? Может же быть так, например, что в программе есть еще что-то, что не дает возможности такой коллизии. По четным секундам читаем десятью потоками, а по нечетным — обновляем в одно лицо. Масса вариантов.

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

        1. Попробуй сравнить производительность. Но тут еще момент — атомарные операции работают только с нативными типами. То есть c int, а не c Int, так что ты можешь потерять что-то от объектной модели.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *