Неустойчивость доступа к общей памяти между запросами в ASP.NET Core

Эрик Липперт упомянул в этом посте в блоге несколько лет назад, что:

Компилятору C#, JIT-компилятору и процессору разрешено выполнять оптимизацию при условии, что оптимизация не обнаруживается текущим потоком. В частности, операции чтения и записи переменных могут быть переупорядочены или полностью исключены при условии, что это не будет наблюдаться в текущем потоке.

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

class Program
{
    static void Main(string[] args)
    {
        int x = 0;
        int z = 0;
        Task.Run(() =>
        {
            Thread.Sleep(1000);
            x = 1;
        });

        while (x == 0)
            unchecked { z = z + 1; }
    }
}

Теперь мне интересно, в серверном приложении, таком как ASP.NET Core, которое по своей сути поддерживает параллелизм и является многопоточным, такое переупорядочивание компилятора/кэширование регистров или любые другие закулисные оптимизации могут привести к тонким ошибкам? Например, предположим, что у меня есть подобная одноэлементная служба, к которой обращаются одновременно, когда сервер обрабатывает запросы:

public class SingletonService
{
    private string _cachedValue;

    public async Task<string> GetStringValue()
    {
        if (IsValueExpired(_cachedValue))
        {
            _cachedValue = await GetNewValue();
        }
        return _cachedValue;
    }

    ...
}

// when configuring the DI services, e.g. in Startup.cs
services.AddSingleton<SingletonService>;

Возможно ли, чтобы мутация _cachedValue никогда не наблюдалась потоком обработки запросов, даже спустя долгое время после того, как значение было обновлено другим потоком? Если это так, то это означает, что этот тип кэширования подвержен тонким и трудно воспроизводимым ошибкам, и мне, вероятно, потребуется заключить чтение/запись _cachedValue в блокировки (которые, как я понимаю, обеспечивают барьеры памяти, которые гарантируют что мутации сбрасываются во все потоки).

В частности, я хотел бы знать, является ли комментарий Эрика выше по-прежнему правильным или актуальным в .NET Core/.NET 5+ и мире async/await сегодня? И если да, то как лучше всего кэшировать такие вещи в памяти в ASP.NET Core? Нужно ли мне для безопасности закрывать весь доступ к общей памяти блокировками?

P.S. Я понимаю разницу между «многозадачностью» и «многопоточностью» и понимаю, что конвейер обработки запросов в ASP.NET Core полностью асинхронен и не привязан к конкретному потоку. Но это не имеет отношения к вопросу выше, поэтому простите меня за использование термина «многопоточность» немного вольно.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
91
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Возможно ли, чтобы изменение _cachedValue никогда не наблюдалось потоком обработки запросов, даже спустя долгое время после того, как значение было обновлено другим потоком?

Возможно, но крайне маловероятно. Если поток запроса выполняет что-либо, кроме цикла проверки значения, он, скорее всего, рано или поздно выдаст загрузку значения.

Мне, вероятно, нужно будет обернуть чтение/запись _cachedValue в блокировки

Я считаю, что volatile должно быть достаточно для этого конкретного случая, но, как упоминает Эрик Липперт, блокировки в целом безопаснее и проще для понимания, чем volatile.

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

Как правило, рекомендуется использовать блокировки всякий раз, когда речь идет о доступе к общей памяти. Мое эмпирическое правило: «если сомневаетесь, используйте замок». Просто избегайте взаимоблокировок. Неоспариваемая блокировка занимает около 25 нс, поэтому вам, вероятно, придется обслуживать огромное количество запросов, прежде чем это станет проблемой, при условии, что вам нужно удерживать блокировку только в течение короткого времени.

Но для кеширования есть встроенный кеш памяти , который, как я полагаю, является потокобезопасным. Существуют также такие классы, как ConcurrentDictionary , которые могут подойти для сценария многопоточного кэширования. Или Lazy<T>, который можно использовать для кэширования значений и, возможно, потокобезопасный.

Спасибо за предложения. Я решил, что хорошей практикой будет активно обеспечивать волатильность, а не полагаться на удачу. Большое спасибо!

scharnyw 18.04.2023 10:48

Хороший ответ; Я бы усилил ваш совет использовать блокировки, а не volatile. Volatile — это острый инструмент для экспертов, которые знают, как, когда и зачем его использовать. Мое эмпирическое правило заключается в том, что если решение головоломки, связанной ниже, не очевидно для вас сразу, то вы (как и я!) не понимаете volatile достаточно хорошо, чтобы использовать его безопасно. ericlippert.com/2014/03/26/reordering-optimizations

Eric Lippert 21.04.2023 19:28

@scharnyw: Самый важный совет в этом ответе - использовать готовые детали, а не изобретать свои собственные. Если вам нужен синглтон с ленивой инициализацией, безопасный для потоков, эксперты Microsoft уже написали для вас правильный и высокопроизводительный синглтон в Lazy<T>. Не тратьте время и силы на неправильную, медленную версию их работы.

Eric Lippert 21.04.2023 19:34

@EricLippert Спасибо, Эрик, за комментарии! Хорошо приняты советы (которые согласуются с предложениями в ваших сообщениях в блоге).

scharnyw 24.04.2023 06:54

Другие вопросы по теме