Насколько безопасно делиться массивом между потоками?

Насколько безопасно делиться массивом между обещаниями, как я сделал это в следующем коде?

#!/usr/bin/env perl6
use v6;

sub my_sub ( $string, $len ) {
    my ( $s, $l );
    if $string.chars > $len {
        $s = $string.substr( 0, $len );
        $l = $len;
    }
    else {
        $s = $string;
        $l = $s.chars;
    }
    return $s, $l;
}

my @orig = <length substring character subroutine control elements now promise>;
my $len = 7;
my @copy;
my @length;
my $cores = 4;
my $p = @orig.elems div $cores;
my @vb = ( 0..^$cores ).map: { [ $p * $_, $p * ( $_ + 1 ) ] };
@vb[@vb.end][1] = @orig.elems;

my @promise;
for @vb -> $r {
    @promise.push: start {
        for $r[0]..^$r[1] -> $i {
            ( @copy[$i], @length[$i] ) = my_sub( @orig[$i], $len );
        }
    };
}
await @promise;

person sid_com    schedule 04.05.2017    source источник
comment
Весь смысл обещаний в том, что обещая что-то вернуть, вы не намеренно возвращаете что-либо полезное из префикса оператора start.   -  person Brad Gilbert    schedule 04.05.2017
comment
Но start делает больше, чем просто что-то возвращает. Меня интересует параллелизм для параллельного выполнения кода, чтобы все ядра моего процессора работали.   -  person sid_com    schedule 04.05.2017
comment
То, что я говорю, похоже на то, как взять гаечный ключ и затем забить им гвоздь. Что работает… я полагаю.   -  person Brad Gilbert    schedule 04.05.2017


Ответы (4)


Это зависит от того, как вы определяете «массив» и «долю». Что касается массивов, есть два случая, которые необходимо рассматривать отдельно:

  • Массивы фиксированного размера (объявлено my @a[$size]); сюда входят многомерные массивы с фиксированными размерами (например, my @a[$xs, $ys]). У них есть интересное свойство, заключающееся в том, что размер поддерживающей их памяти никогда не нужно изменять.
  • Динамические массивы (объявленные my @a), которые растут по запросу. Под капотом они фактически используют некоторое количество блоков памяти с течением времени по мере их роста.

Что касается обмена, есть также три случая:

  • Случай, когда несколько потоков соприкасаются с массивом за время его существования, но только один может касаться его одновременно из-за какого-либо механизма управления параллелизмом или общей структуры программы. В этом случае массивы никогда не используются совместно в смысле «параллельных операций с использованием массивов», поэтому нет возможности иметь гонку данных.
  • Вариант только для чтения, неленивый. Здесь несколько одновременных операций обращаются к неленивому массиву, но только для его чтения.
  • Случай чтения / записи (в том числе, когда чтение действительно вызывает запись, потому что массиву было назначено что-то, что требует ленивой оценки; обратите внимание, что это никогда не может произойти для массивов фиксированного размера, поскольку они никогда не ленивы).

Тогда мы можем резюмировать безопасность следующим образом:

                     | Fixed size     | Variable size |
---------------------+----------------+---------------+
Read-only, non-lazy  | Safe           | Safe          |
Read/write or lazy   | Safe *         | Not safe      |

Знак * указывает на то, что, хотя это безопасно с точки зрения Perl 6, вы, конечно, должны убедиться, что не делаете противоречивых вещей с одними и теми же индексами.

Таким образом, массивы фиксированного размера, которые вы можете безопасно совместно использовать и назначать элементам из разных потоков, «не проблема» (но будьте осторожны с ложным совместным использованием, которое может заставить вас заплатить за это серьезное снижение производительности). Для динамических массивов это безопасно только в том случае, если они будут считываться только в течение периода, когда они совместно используются, и даже тогда, если они не ленивы (хотя данное назначение массива в основном нетерпеливое, вы вряд ли попадете в эту ситуацию случайно). Запись, даже в различные элементы, может привести к потере данных, сбоям или другому неправильному поведению из-за расширения операций.

Итак, рассматривая исходный пример, мы видим, что my @copy; и my @length; являются динамическими массивами, поэтому мы не должны писать в них в параллельных операциях. Однако такое случается, поэтому код может быть определен как небезопасный.

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

person Jonathan Worthington    schedule 04.05.2017

Просто сделайте так, чтобы код, помеченный префиксом оператора start, возвращал значения, чтобы Perl 6 мог сделайте синхронизацию за вас. В этом весь смысл этой функции.
Затем вы можете дождаться всех обещаний и получить все результаты, используя _ 2_ заявление.

my @promise = do for @vb -> $r {

    start

      do  # to have the 「for」 block return its values

        for $r[0]..^$r[1] -> $i {
            $i, my_sub( @orig[$i], $len )
        }
}

my @results = await @promise;

for @results -> ($i,$copy,$len) {
  @copy[$i] = $copy;
  @length[$i] = $len;
}

Префикс оператора start лишь косвенно связан с параллелизмом.
Когда вы используете если вы говорите: «Мне не нужны эти результаты прямо сейчас, но, вероятно, позже».

По этой причине он возвращает Promise (асинхронность), а не Thread (параллелизм)

Среде выполнения разрешено отложить фактическое выполнение этого кода до тех пор, пока вы, наконец, не запросите результаты, и даже тогда она может просто выполнить все из них последовательно в одном потоке.

Если бы реализация действительно это сделала, это могло бы привести к чему-то вроде тупика, если вы вместо этого опросите Promise постоянно вызывая его метод .status, ожидая его изменения с Planned на Kept или Broken, и только затем запросите его результат.
Это одна из причин, по которой планировщик по умолчанию начнет работать на любом Обещайте коды, если у него есть свободные потоки.


Я рекомендую посмотреть доклад jnthn «Параллелизм, параллелизм и асинхронность в Perl 6».
слайды

person Brad Gilbert    schedule 04.05.2017
comment
Возврат значений (для использования await) и последующее копирование значений в нужное место немного замедлили работу. Кроме того, мне труднее читать код. Я пробовал использовать Thread интерфейс; Прироста скорости я не увидел и он ниже. - person sid_com; 05.05.2017
comment
@sid_com Помещение его в массив заставляет его ждать перед обработкой цикла. Если вы просто поместите await в цикл for, где находится @results, он должен начать обработку значений до того, как все они будут выполнены. - person Brad Gilbert; 05.05.2017

Этот ответ относится к моему пониманию ситуации с MoarVM, я не знаю, каково состояние бэкэнда JVM (или fwiw бэкэнда Javascript).

  • Чтение скаляра из нескольких потоков может выполняться безопасно.
  • Изменить скаляр из нескольких потоков можно, не опасаясь ошибки сегментации, но вы можете пропустить обновления:

$ perl6 -e 'my $i = 0; await do for ^10 { start { $i++ for ^10000 } }; say $i' 46785

То же самое относится к более сложным структурам данных, таким как массивы (например, отправляются отсутствующие значения) и хэши (добавляются отсутствующие ключи).

Итак, если вы не против пропустить обновления, изменение общих структур данных из нескольких потоков должно работать. Если вы не возражаете против пропуска обновлений, чего, я думаю, вы обычно и хотите, вам следует взглянуть на настройку своего алгоритма другим способом, как это предлагают @Zoffix Znet и @raiph.

person Elizabeth Mattijsen    schedule 04.05.2017

No.





Серьезно. Другие ответы, похоже, делают слишком много предположений о реализации, ни одно из которых не проверяется спецификацией.

person Aleks-Daniel Jakimenko-A.    schedule 08.05.2017