С# параллелизм массива структур

Учитывая массив структуры:

public struct Instrument
{
    public double NoS;
    public double Last;
}

var a1 = new Instrument[100];

И пул многопоточных задач, который выполняет запись в эти элементы на основании того, что один элемент может быть записан не более чем двумя потоками одновременно, по одному для каждого из двойных полей (существует эффективная очередность восходящего потока по теме).

И знание того, что двойные числа могут быть записаны атомарно на 64-битной системе. (отредактируйте это, ошибочно сказав, что 32 бит изначально)

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

Итак, я могу сделать снимок массива с помощью:

var snapshot = a1.Clone();

Теперь вопрос, который у меня есть, касается особенностей синхронизации. Если я сделаю члены изменчивыми, я не думаю, что это вообще поможет клонированию, так как чтение/запись aquire/releases не на уровне массива.

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

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

Теперь на самом деле не имеет значения, имеет ли моментальный снимок самое последнее значение, если это вопрос микросекунд и т. д., поэтому я думаю, что, вероятно, мне могло бы сойти с рук просто отсутствие блокировки. Единственное, что меня беспокоит, это то, может ли быть сценарий, при котором обратная запись кеша не выполняется в течение длительного периода времени. Это то, о чем я должен беспокоиться? Писатели находятся в потоке данных TPL, и единственная логика состоит в том, чтобы установить два поля в структуре. Я действительно не знаю, как или если область действия имеет тенденцию коррелировать с обратной записью в кеш.

Мысли/совет?

edit: что, если бы я использовал блокировку записи в переменные в структуре?

edit2: объем операций записи НАМНОГО выше, чем объем чтения. Есть также две отдельные и параллельные службы, записывающие в поля Nos & Last. Таким образом, они могут быть написаны одновременно одновременно. Это вызывает проблемы с подходом ссылочного объекта для атомарности.

edit3: дополнительная информация. Предположим, массив состоит из 30-1000 элементов, и каждый элемент может обновляться несколько раз в секунду.


person DanH    schedule 12.06.2012    source источник
comment
ПРЕДУПРЕЖДЕНИЕ. Двойное значение является 64-разрядным значением и не может быть записано атомарно на 32-разрядных машинах: msdn.microsoft.com/en-us/library/system.double.aspx.   -  person Steven    schedule 12.06.2012
comment
Стивен, хороший улов, спасибо! Я работал на этой основе, пока коллега не сбил меня с толку прошлой ночью. Хорошая точка зрения.   -  person DanH    schedule 12.06.2012
comment
редактирование: блокировка поможет только в том случае, если все доступа используют блокировку. Копия памяти (семантика копирования структуры) этого не сделает, поэтому вы все равно можете получить порванное состояние.   -  person Marc Gravell    schedule 12.06.2012
comment
Рассматривали ли вы возможность сделать Instrument неизменяемой структурой? Таким образом, вы можете читать в любое время без блокировок или с очень небольшим количеством блокировок (только для метода Clone).   -  person Dr. Andrew Burnett-Thompson    schedule 12.06.2012
comment
@Dr.ABT Я согласен, что это упрощает обработку структуры, но на самом деле это не решает проблему чтения одним потоком, в то время как другой поток записывает в массив (заменяя значение) - вы можете все еще получить разорванное (неатомарное) состояние   -  person Marc Gravell    schedule 12.06.2012
comment
@Dr.ABT посмотрите ответ, который я только что добавил, о том, чтобы сделать его классом, который решает эту проблему.   -  person Marc Gravell    schedule 12.06.2012
comment
У меня есть подозрение, что Спрашивающий работает в финансовой сфере, где им нравятся структуры за их высокую пропускную способность с минимальными сборщиками мусора. Честно говоря, я лично был свидетелем того, как небольшие неизменяемые классы превосходят структуры, поэтому согласен с решением. Никаких блокировок, никаких споров, просто чтение   -  person Dr. Andrew Burnett-Thompson    schedule 12.06.2012
comment
так что проблема с использованием классов вместо структур заключается в том, что у меня есть две разные службы, которые независимо пишут в поля NoS и Last. Это означает, что у меня есть серьезная проблема с переключением объектов для атомарности, и я не понимаю, как я мог бы сделать снимок без блокировки высокого уровня.   -  person DanH    schedule 12.06.2012
comment
@MarcGravell Могу ли я получить разорванное состояние на двойном уровне, если я использую блокировку? Меня действительно не волнует, является ли I NoS устаревшим или последним, пока фактические двойники не разорваны.   -  person DanH    schedule 12.06.2012
comment
@DanH При назначении структуры, если есть несколько потоков, то да, она, вероятно, порвется. Что касается того, где она рвется: кто знает. Я не могу гарантировать поведение, используя спецификацию языка, поэтому я не могу рекомендовать это делать. Если вы никогда не назначаете, а вместо этого всегда используете interlocked для обновления на месте внутри массива (через множество ref), то, что ж, это, вероятно, сработает, но опять же , это вносит сложность. Подход с неизменяемым классом значительно проще.   -  person Marc Gravell    schedule 12.06.2012
comment
Immutable не работает, потому что у меня есть два потока, записывающих в структуры по одному для каждой переменной.   -  person DanH    schedule 12.06.2012
comment
@Steven: вы можете найти этот ТАК вопрос представлять интерес. В частности, этот ответ   -  person Brian    schedule 13.06.2012
comment
Если я сделаю участников непостоянными, я не думаю, что это поможет. volatile вряд ли вам когда-нибудь поможет. Прочтите статью Эрика Липперта о Атомарность, волатильность и неизменность. Он отговаривает вас от создания изменчивого поля.   -  person Steven    schedule 13.06.2012


Ответы (5)


Поскольку Instrument содержит два двойника (два 64-битных значения), вы не можете записать его атомарно (даже на 64-битных машинах). Это означает, что метод Clone никогда не сможет создать потокобезопасную копию без какой-либо синхронизации.

TLDR; Не используйте структуру, используйте неизменяемый класс.

Возможно, вам больше повезет с небольшим редизайном. Попробуйте использовать неизменяемые структуры данных и параллельные коллекции из платформы .NET. Например, сделайте свой Instrument неизменяемым классом:

// Important: Note that Instrument is now a CLASS!! 
public class Instrument
{
    public Instrument(double nos, double last)
    {
        this.NoS = nos;
        this.Last = last;
    }

    // NOTE: Private setters. Class can't be changed
    // after initialization.
    public double NoS { get; private set; }
    public double Last { get; private set; }
}

Таким образом, обновление Instrument означает, что вам нужно создать новый, что значительно упрощает рассмотрение этого вопроса. Когда вы уверены, что только один поток работает с одним Instrument, все готово, так как рабочий теперь может безопасно это сделать:

Instrument old = a[5];

var newValue = new Instrument(old.NoS + 1, old.Last - 10);

a[5] = newValue;

Поскольку ссылочные типы являются 32-разрядными (или 64-разрядными на 64-разрядной машине), обновление ссылки гарантированно будет атомарным. Клон теперь всегда будет давать правильную копию (может не хватать, но для вас это не проблема).

ОБНОВЛЕНИЕ

Перечитав ваш вопрос, я вижу, что неправильно его понял, так как один поток пишет не в Instrument, а пишет в значение инструмента, но решение практически одно и то же: использовать неизменяемые ссылочные типы. Один простой трюк, например, состоит в том, чтобы изменить вспомогательные поля свойств NoS и Last на объекты. Это делает их обновление атомарным:

// Instrument can be a struct again.
public struct Instrument
{
    private object nos;
    private object last;

    public double NoS
    {
        get { return (double)(this.nos ?? 0d); }
        set { this.nos = value; }
    }

    public double Last
    {
        get { return (double)(this.last ?? 0d); }
        set { this.last = value; }
    }
}

При изменении одного из свойств значение будет помещено в рамку, а значения в рамке являются неизменяемыми ссылочными типами. Таким образом, вы можете безопасно обновить эти свойства.

person Steven    schedule 12.06.2012
comment
(комментарий) Действительно, я не видел этого - проголосовал и удалил свой младший дубликат. - person Marc Gravell; 12.06.2012
comment
Это не работает в моем случае использования, так как две службы записывают в каждый дубль соответственно. Таким образом, создание экземпляра нового экземпляра ref вызовет условия гонки, требующие мелкозернистой блокировки. Я мог бы иметь две ссылки на двойников (например, боксировать их), но я думаю, что идти по этому пути становится немного нелепо. - person DanH; 12.06.2012

И знание того, что двойные числа могут быть записаны атомарно на 32-битных.

Нет, это не гарантируется:

12.5 Атомарность ссылок на переменные

Чтение и запись следующих типов данных должны быть атомарными: bool, char, byte, sbyte, short, ushort, uint, int, float и ссылочные типы. Кроме того, операции чтения и записи перечислимых типов с базовым типом из предыдущего списка также должны быть атомарными. Чтение и запись других типов, включая long, ulong, double и decimal, а также определяемые пользователем типы, не обязательно должны быть атомарными.

(выделено мной)

Никаких гарантий в отношении удвоения на 32-битной или даже на 64-битной системе нет. strcut, состоящий из 2 дублей, еще более проблематичен. Вам следует пересмотреть свою стратегию.

person Marc Gravell    schedule 12.06.2012
comment
Каков источник этой цитаты? - person Steven; 12.06.2012
comment
@ Стивен, это ECMA 334 v4. Я также могу проверить спецификацию MS, если хотите. - person Marc Gravell; 12.06.2012
comment
@ Стивен, спецификация MS говорит то же самое, но нумерация другая - это раздел 5.5. - person Marc Gravell; 12.06.2012
comment
Я с вами не спорю, но было бы неплохо увидеть источник той цитаты, или в воспитательных целях. Ссылка, которую стоит оценить. - person Steven; 12.06.2012
comment
Хорошая загвоздка, однако, в том, что теоретически запись двойников в 64-битной среде не является атомарной, хотя я ожидаю, что это будет иметь место на практике. Однако всегда разумно придерживаться спецификации. - person Steven; 12.06.2012
comment
@Стивен, просто найдите спецификацию языка C#; p msdn.microsoft.com/en-us /library/ms228593.aspx имеет ссылку на него как HTML, ищите Атомарность ссылок на переменные - person Marc Gravell; 12.06.2012
comment
Я отредактировал вопрос, чтобы отразить 64-битную безопасность, но спасибо, что указали, что я не должен предполагать, что 64-битная версия тоже атомарна. - person DanH; 12.06.2012
comment
Итак, давайте предположим, что я использую блокировку записи. - person DanH; 12.06.2012

Вы можете (ab) использовать ReaderWriterLockSlim.

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

Не уверен, что я бы сделал это, если бы действительно не было альтернативы. Может сбивать с толку тех, кто поддерживает это в будущем.

person Joe    schedule 12.06.2012
comment
В большинстве случаев ReaderWriterLockSlim выполняется медленнее, чем простой оператор lock. ReaderWriterLockSlim особенно полезен, когда время, затрачиваемое на операцию чтения, велико (например, при выполнении ввода-вывода), но, вероятно, в данной ситуации это не так. - person Steven; 12.06.2012
comment
@ Стивен, возможно, ты прав, но авторитетная ссылка поможет убедить. В следующем блоге предполагается, что производительность примерно равна производительности монитора: noreferrer">bluebytesoftware.com/blog/ В случае с OP количество блокировок чтения значительно превышает количество блокировок записи, обновление и рекурсия не требуются, и я ожидаю, что ReaderWriterLockSlim будет работать хорошо. - person Joe; 12.06.2012
comment
Взгляните на эту (более новую) статью того же автора, Джо Даффи: Блокировки чтения-записи и их неприменимость к тонкой синхронизации. Цитата: Серьезно задайтесь вопросом, действительно ли блокировка чтения/записи что-нибудь вам купит. - person Steven; 12.06.2012

Чтение и запись отдельных элементов массива или отдельных полей структуры, как правило, независимы. Если в то время как один поток записывает конкретное поле определенного экземпляра структуры, никакой другой поток не будет пытаться получить доступ к тому же полю, массив структур будет неявно потокобезопасным без какой-либо блокировки, необходимой помимо логики, которая обеспечивает выполнение вышеуказанных условий.

Если возможно, что один поток может попытаться прочитать double, в то время как другой поток записывает его, но невозможно, чтобы два потока могли пытаться писать одновременно, существует ряд подходов, которые вы можете предпринять, чтобы гарантировать, что чтение не будет выполнено. см. частично написанное значение. Один из них, который еще не упоминался, заключается в определении поля int64 и использовании пользовательских методов для чтения и записи в него значений double (побитовое преобразование и использование Interlocked по мере необходимости).

Другой подход состоит в том, чтобы иметь переменную changeCount для каждого слота массива, которая увеличивается, так что два LSB равны «10» до того, как будет записана структура, и Interlocked.Increment на 2 после нее (см. примечание ниже). Прежде чем код прочитает структуру, он должен проверить, выполняется ли запись. Если нет, он должен выполнить чтение и убедиться, что запись не началась или не произошла (если запись произошла после начала чтения, вернуться к началу). Если запись выполняется, когда код хочет прочитать, он должен получить общую блокировку, проверить, выполняется ли запись, и если это так, использовать операцию блокировки, чтобы установить LSB changeCount и Monitor.Wait в блокировке. Код, написавший структуру, должен заметить в своем Interlocked.Increment, что LSB установлен, и должен Pulse заблокировать. Если модель памяти гарантирует, что чтение одним потоком будет обрабатываться по порядку, а запись одним потоком будет обрабатываться по порядку, и если только один поток будет когда-либо пытаться записать слот массива за раз, этот подход должен ограничьте многопроцессорные накладные расходы одной операцией Interlocked в случае отсутствия конфликтов. Обратите внимание, что перед использованием такого кода необходимо тщательно изучить правила о том, что подразумевается или не подразумевается моделью памяти, поскольку это может быть сложно.

Кстати, есть еще два подхода, которые можно было бы использовать, если бы вы хотели, чтобы каждый элемент массива был типом класса, а не структурой:

  1. Use an immutable class type, and use `Interlocked.CompareExchange` any time you want to update an element. The pattern to use is this:
      MyClass oldVal,newVal;
      do
      {
        oldVal = theArray[subscript];
        newVal = new MyClass(oldVal.this, oldVal.that+5); // Or whatever change
      } while (Threading.Interlocked.CompareExchange(theArray[subscript], newVal, oldVal) != oldVal);
    
    This approach will always yield a logically-correct atomic update of the array element. If, between the time the array element is read and the time it is updated, something else changes the value, the `CompareExchange` will leave the array element unaffected, and the code will loop back and try again. This approach works reasonably well in the absence of contention, though every update will require generating a new object instance. If many threads are trying to update the same array slot, however, and the constructor for `MyClass` takes any significant amount of time to execute, it's possible for code to thrash, repeatedly creating new objects and then finding out they're obsolete by the time they could be stored. Code will always make forward progress, but not necessarily quickly.
  2. Use a mutable class, and lock on the class objects any time one wishes to read or write them. This approach would avoid having to create new class object instances any time something is changed, but locking would add some overhead of its own. Note that both reads and writes would have to be locked, whereas the immutable-class approach only required `Interlocked` methods to be used on writes.

Я склонен думать, что массивы структур лучше подходят для хранения данных, чем массивы объектов класса, но оба подхода имеют свои преимущества.

person supercat    schedule 12.06.2012
comment
Спасибо. Определенно оптимизация слишком далеко в моем случае! Почти уверен, что я потратил бы дни, чтобы заставить это работать. - person DanH; 13.06.2012
comment
@DanH: Минимальный-Interlocked подход был бы довольно грубым, и я бы не рекомендовал его, если только вам не нужна супер-пупер-производительность. Я бы хотел, чтобы Framework предоставила Interlocked перегрузки для double; Я действительно не вижу никаких причин, по которым это не могло бы быть сделано, поскольку базовые инструкции ЦП не заботились бы о том, чтобы указатель на число с плавающей запятой был приведен к указателю на число int64. - person supercat; 13.06.2012
comment
@DanH: добавлено еще два предложения. - person supercat; 13.06.2012

Хорошо, так что подумал об этом за обедом.

Я вижу два, возможно, 3 решения здесь.

Первое важное замечание: идея неизменяемости не работает в моем случае использования, потому что у меня есть две службы, работающие параллельно и записывающие в NoS и Last независимо друг от друга. Это означает, что мне потребуется дополнительный уровень логики синхронизации между этими двумя службами, чтобы гарантировать, что пока новая ссылка создается одной службой, другая не делает то же самое. Классическая проблема состояния гонки, поэтому определенно не подходит для этой проблемы (хотя да, я мог бы иметь рефери для каждого дубля и делать это таким образом, но в этот момент это становится смешно)

Решение 1 Блокировка всего кэша. Возможно, используйте спин-блокировку и просто заблокируйте все обновления и моментальный снимок (с помощью memcpy). Это самое простое и, вероятно, совершенно нормальное для томов, о которых я говорю.

Решение 2. Сделайте так, чтобы при записи в двойники использовалась блокированная запись. когда я хочу сделать снимок, повторите массив и каждое значение, используя блокированное чтение, чтобы заполнить копию. Это может привести к разрыву структуры, но двойники остаются нетронутыми, и это нормально, поскольку данные постоянно обновляются, поэтому понятие последних немного абстрактно.

Решение 3 Не думаю, что это сработает, но как насчет блокировки записи во все дубликаты, а затем просто использования memcopy. Я не уверен, что я получу разрыв двойников, хотя? (помните, меня не волнует разрыв на уровне структуры).

Если решение 3 работает, то я предполагаю, что это лучшая производительность, но в остальном я больше склоняюсь к решению 1.

person DanH    schedule 12.06.2012
comment
Итак, решение 3 не работает: stackoverflow. ком/вопросы/10998730/ - person DanH; 12.06.2012