Как в многопоточном приложении безопасно прочитать 16-байтовое структурное значение без использования блокировок?

Есть такой сценарий: общая таблица cache (Note[]), к которой имеют доступ все потоки. Запись в таблицу случается редко и выполняется под блокировкой, чтение должно быть максимально быстрым и не учитывать многопоточность читателя.
  • 30 марта 2026 г. 20:01
Ответы на вопрос 2
Коротко: нельзя надёжно читать произвольный 16‑байтовый struct «атомарно» просто так — CLR не гарантирует атомарность чтения/записи struct размером > указателя/long. Но есть простые шаблоны, дающие безопасное безблокировочное чтение:

1) Самый простой и рекомендуемый — хранить данные как immutable-ссылку и атомически публиковать новую ссылку.  
2) Если вы обязаны хранить struct, — использовать seqlock (versioned read) — читатель читает версию до/после чтения данных и повторяет, пока версия не совпадёт.  
3) Ещё вариант — держать всю таблицу как immutable-массив и атомически менять ссылку на массив.

Ниже примеры на C#.

1) Immutable object (рекомендуется)
- Сделайте Note классом с readonly-полями (или record/immutable struct как class-эквивалент).
- При обновлении создавайте новый объект и делайте Interlocked.Exchange/Volatile.Write для публикации.
- Читатели просто читают ссылку — чтение ссылок атомарно и с правильно оформленной семантикой памяти безопасно.

Пример:
```csharp
class Note {
    public readonly int A;
    public readonly int B;
    public Note(int a, int b) { A = a; B = b; }
}

class Cache {
    // массив ссылок на immutable Note
    private readonly Note[] _notes;

    public Cache(int size) { _notes = new Note[size]; }

    public void Update(int idx, Note newNote) {
        // атомически заменим ссылку
        System.Threading.Interlocked.Exchange(ref _notes[idx], newNote);
    }

    public Note Read(int idx) {
        // безопасно читать ссылку; Volatile.Read можно использовать для явного барьера
        return System.Threading.Volatile.Read(ref _notes[idx]);
    }
}
```
Плюсы: очень просто, читатели супербыстрые (обычное чтение ссылки). Минусы: требуется аллокация нового объекта при каждом обновлении (обычно не проблема, если записи редки).

2) Seqlock (версионирование) — для struct размером 16 байт
Идея: рядом с данными держим целочисленный счётчик версии. Писатель:
- увеличивает версию (делает её нечётной) — это пометка «в процессе записи»,
- записывает данные,
- снова увеличивает версию (делает чётной) — «запись завершена».

Читатель:
- читает версию s1,
- если s1 нечётное — повторить,
- копирует данные (может быть torn),
- читает версию s2,
- если s1 == s2 (и чётное) — данные консистентны; иначе — повторить.

Пример:
```csharp
using System.Threading;

struct Note { public long X; public long Y; } // пример 16 байт

class Cell {
    private long _seq;      // version counter
    private Note _value;

    public void Write(Note newVal) {
        Interlocked.Increment(ref _seq);   // теперь _seq нечётный (write in progress)
        // гарантируем порядок: запись данных после увеличения seq
        Volatile.Write(ref _value, newVal);
        Interlocked.Increment(ref _seq);   // теперь чётный (write finished)
    }

    public Note Read() {
        for (;;) {
            long s1 = Volatile.Read(ref _seq);
            if ((s1 & 1) != 0) continue;           // писатель вовремя
            Note copy = _value;                    // тут может быть torn, но тогда seq изменится
            long s2 = Volatile.Read(ref _seq);
            if (s1 == s2) return copy;
        }
    }
}
```
Важно:
- Используйте Volatile.Read/Write или MemoryBarrier, чтобы обеспечить нужный порядок операций.
- Seqlock — очень быстрый для читателей, но читатель может зацикливаться, если писатель интенсивно переписывает (у вас писатель редкий — подходит).
- Seqlock даёт «wait-free» чтение (в смысле без блокировок), но не дает гарантии обхода starvation при постоянных писателях.

3) Replace whole array
Если можно — делайте всю таблицу immutable и заменяйте ссылку на массив целиком:
- создаёте новый массив с копией и новым значением,
- публикуете новый массив через Interlocked.Exchange(ref cacheArray, newArray).
Читатели читают ссылку на массив и обращаются без синхронизации. Это удобно, если массив небольшой и записи редки.

Заключение и рекомендации
- Если вы можете — используйте immutable-ссылку (вариант 1) — проще и безопаснее. Чтение максимально быстрое.
- Если хотите избегать аллокаций и обязаны хранить struct — используйте seqlock (вариант 2), но аккуратно с памятью/барьерами.
- Всегда учитывайте архитектуру (32/64 bit) и используйте Volatile/Interlocked для корректных барьеров и атомарных операций.

Если скажете, на каком языке/платформе вы работаете и как выглядит Note (struct или class, поля), дам точный минимальный пример под вашу ситуацию.
У тебя тут прям книжный пример гонки. <br/> Используй System.Threding.Interlocked, чтобы гарантировать порядок чтения/записи. <br/> MemoryFence очень редко когда нужен - это раз. <br/> Его нужно использовать и на стороне чтения тоже  - это два. <br/> <br/> <blockquote><br/>
могут ли операции чтения переставляется<br/>
</blockquote> <br/> Могут и переставляться, и кэшироваться, и удаляться "лишние" чтения по мнению компилятора, если не обмазываться атомиками. <br/> <blockquote><br/>
но чтение должно не знать, что оно в многопоточной среде и должно быть максимально быстрым.<br/>
</blockquote> <br/> Вспоминается анекдот: <br/> <blockquote><br/>
Сколько будет 453 * 774? <br/>
954755 - ответил без раздумий. <br/>
Но это же не правильно... <br/>
Зато быстро!<br/>
</blockquote> <br/> <br/> <blockquote><br/>
Такой асемблерный код генерится<br/>
</blockquote> <br/> Завтра, когда обновиться компилятор, рантайм, JIT соберёт другую статистику, программа будет запущена на другом компьютере, меркурий будет в Козероге - сгенерится другой код. <br/> <br/> <blockquote><br/>
Если вдруг да, то есть ли какой-то алгоритм способ, техника, или атрибут, который может гарантировать что все на месте<br/>
</blockquote> <br/> Interlocked либо уже готовые структуры типа ConcurrentDictionary. <br/> <br/> Если добавить контекста: <br/> 1. Сколько потоков пишут <br/> 2. Сколько потоков читают <br/> 3. Как вообще эти данные используются <br/> <br/> То можно будет уже говорить об оптимальном решении.
Похожие вопросы