Многопоточный доступ к статическому объекту нестатического класса

#c# #multithreading #thread-safety #concurrentdictionary

#c# #многопоточность #потокобезопасность #concurrentdictionary

Вопрос:

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

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

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

 public class Profile {
    private ConcurrentDictionary<int, int> cache = 
                              new ConcurrentDictionary<int, int>();

    public AddToCache() {

    }
    public RemoveToCache() {

    }
    public DoSomethingThatShouldBeThreadSafe() {

    }
}
  

Но затем я создаю статический объект из этого класса.

 public static Profile objProfile = new Profile();
  

И затем доступ к objProfile осуществляется с помощью нескольких потоков.

Вопрос в том, будут ли методы класса Profile, AddToCache, RemoveFromCache и DoSomethingThatShouldBeThreadSafe потокобезопасными или нет при использовании через objProfile? Будут ли их переменные совместно использоваться потоками, даже если они не статичны, потому что весь экземпляр класса статичен?

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

1. У вас должны быть очень специальные нестатические методы, которые волшебным образом улучшают все ваши переменные-члены, чтобы быть потокобезопасными 🙂

2. открытый статический профиль objProfile = new Profile(); неверно.

3. На самом деле я использую эту инициализацию долгие годы. Можете ли вы пояснить, что, по вашему мнению, в этом неверно?

Ответ №1:

Пока вы обращаетесь только к ConcurrentDictionary<> экземпляру cache и не перезаписываете cache новый экземпляр одним из Profile методов, он является потокобезопасным.

Из-за второго пункта лучше пометить его readonly ,

 private readonly ConcurrentDictionary<int, int> cache = 
                     new ConcurrentDictionary<int, int>();
  

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


Редактировать:

Хотя ConcurrentDictionary<> сам по себе потокобезопасен, у вас все еще есть проблема неатомности составных операций. Давайте рассмотрим два возможных GetFromCache() метода.

 int? GetFromCacheNonAtomic(int key)
{
    if (cache.ContainsKey(key))    // first access to cache
        return cache[key];         // second access to cache

    return null;
}

int? GetFromCacheAtomic(int key)
{
    int value;

    if (cache.TryGetValue(key, out value))   // single access to cache
        return value;

    return null;
}
  

только второй является атомарным, потому что он использует ConcurrentDictionary<>.TryGetValue() метод.


ПРАВКА 2 (ответ на 2-й комментарий Чао):

ConcurrentDictionary<> имеет GetOrAdd() метод, который принимает Func<TKey, TValue> делегат для несуществующих значений.

 void AddToCacheIfItDoesntExist(int key)
{
    cache.GetOrAdd(key, SlowMethod);
}

int SlowMethod(int key)
{
    Thread.Sleep(1000);
    return key * 10;
}
  

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

1. Он не предназначался для перезаписи, но вы правы, нельзя быть слишком осторожным 🙂 Спасибо.

2. Я всегда стараюсь использовать методы Try *** для атомарности. Но есть одна вещь, которую я не могу обойти. Допустим, я должен добавить объект в параллельный словарь, но этому объекту требуется подключение к базе данных для сложной sp, которая тратит много ресурсов для возврата необходимого значения. Итак, я иду: if (!кэш. Содержит ключ) { кэш. TryAdd(int, DBOperation); } Любая идея будет высоко оценена, поскольку это единственное, что меня не устраивает в моих кодах в проекте…

3. Тупой я, я никогда не думал о GetOrAdd (), использующем подобное. для(int i=0;i<int.MaxValue;i ) { Спасибо ulrichb } 🙂

4. Для GetOrAdd() является важным замечанием в документации: If you call GetOrAdd simultaneously on different threads, addValueFactory may be called multiple times, but its key/value pair might not be added to the dictionary for every call.

Ответ №2:

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

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

Ответ №3:

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

Однако кэш будет только ОДИН, поэтому статическое использование содержащего класса сделает обращение к кэшу небезопасным для потоков.