Внимание: код в этом посте требует 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
Вот это порвало голову. . Юникод модификатор ..
Это всё хорошо но… А зачем вообще допускать существование неатомарных операций в условиях распараллеливания задач?
Они же не всегда нужны.
Позволь уточнить: т.е. есть задачи, когда подобный разрыв чтения-записи, как в первом примере, допустим. Мне в голову приходит только что-нибудь в духе, когда каждый раз нужно сделать новую запись; пардон, с этой темой я только сегодня столкнулся, поэтому вопрошаю: Я в верном направлении мыслю?
Почему обязательно разрыв? Может же быть так, например, что в программе есть еще что-то, что не дает возможности такой коллизии. По четным секундам читаем десятью потоками, а по нечетным — обновляем в одно лицо. Масса вариантов.
Синтаксис, имхо, не очень удачный: по идее, компилятор может сам догадаться, раз я объявил переменную atomic. Но все равно это не должно распространяться на все переменные.
Хорошо. Тогда другой вопрос на ту же тему: как использование атомарной операции может мне навредить?
Попробуй сравнить производительность. Но тут еще момент — атомарные операции работают только с нативными типами. То есть c int, а не c Int, так что ты можешь потерять что-то от объектной модели.
Окей, спасибо!