Являются ли статические переменные потокобезопасными в том смысле, что вы можете читать/писать из нескольких потоков без сбоев?

#c# #multithreading #asp.net-core #.net-core

Вопрос:

У меня есть свойство, определенное как:

 private static MyStaticCache? _myStaticCache;
 

MyStaticCache является классом со строковым свойством. Доступ могут получить несколько потоков _myStaticCache . Если свойство равно null или значение старое, любой поток, обращающийся к нему, получит значение из источника и установит _myStaticCache это значение.

 public string GetValue()
{
    if (_myStaticCache == null || _myStaticCache.CacheIsStale())
        _myStaticCache = GetValueFromSource();

    return _myStaticCache.Value1;
}
 

Ни один код не может и никогда _myStaticCache не будет возвращать значение null.

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

Мой вопрос таков. Есть ли способ, чтобы это привело к сбою? Является ли присвоение _myStaticCache атомарным, или может быть чтение в середине записи?

Тот факт, что метод источника может быть вызван N раз параллельно, на самом деле не имеет значения. Тайм-аут для кэша составляет 30 дней, и очень маловероятно, что в это точное время будет запущено несколько потоков, хотя это и не невозможно, и даже если параллельно будет запущено 100 потоков, вызывающих его, метод вернет одно и то же значение для каждого из них и сможет без проблем справиться с нагрузкой.

Теперь я мог бы использовать мьютекс или обернуть чтение и запись, lock() но я думаю, что это снизит производительность в 99,999% случаев, когда вызывается этот метод, опять же, он будет пустым или устаревшим каждые 30 дней.

Комментарии:

1. Одним словом: нет.

2. @user207421 любой односложный ответ на эту тему, как правило, не стоит потраченного впустую одного слова

3. Может GetValueFromSource вернуться null ?

4. если value это коллекция, она рухнет. существуют десятки ситуаций, когда вещи ведут себя не так, как «ожидалось», когда дело доходит до тяжелой многопоточности. и даже если он не разобьется — вы получите очень сложные условия гонки, когда value вернетесь не таким, как вы ожидали. О, просто прочитайте это еще раз — вы не можете поделиться строкой без синхронизации. Период.

Ответ №1:

Пока MyStaticCache это class (или интерфейс; в основном: ссылочный тип), с вами все должно быть в порядке.

Спецификация языка гарантирует, что вы никогда не получите разорванные ссылки, так что нет, это никогда не должно привести к сбою:

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

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

Так что до тех пор, пока вас не беспокоит многократное выполнение медленного пути для начального / устаревшего значения или проблемы, связанные с использованием регистра и т. Д. : С вами все должно быть в порядке.

Лично я бы, вероятно CacheIsStale() , проверил таймер (с частотой по вашему выбору), который устанавливает поле в null положение, когда оно обнаруживает устаревание, а не постоянно проверяет два условия. Вы даже можете выполнить обновление внутри обратного вызова таймера и вместо этого установить статическое поле в новую ссылку, так что:

 public string GetValue()
    => (_myStaticCache ?? FetchAndAssign()).Value1;
private MyStaticCache FetchAndAssign()
    => _myStaticCache = GetValueFromSource();
private void TimerCallback()
{
    if (_myStaticCache is null || _myStaticCache.CacheIsStale())
        FetchAndAssign();
}
 

Обратите внимание, что мы можем комментировать только _myStaticCache то, что .Value1 зависит от вашего кода, и может быть или не быть потокобезопасным.

Ответ №2:

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

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

https://docs.microsoft.com/en-us/dotnet/api/system.threading.readerwriterlockslim?view=net-5.0

 private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();

    public MyValueType ReadValue()
    {
        cacheLock.EnterReadLock();
        try
        {
            return myCache.Value1;
        }
        finally
        {
            cacheLock.ExitReadLock();
        }
    }

    public void WriteValue(MyValueType value)
    {
        cacheLock.EnterWriteLock();
        try
        {
            myCache.Value1 = value;
        }
        finally
        {
            cacheLock.ExitWriteLock();
        }
    }