Зачем блокировать при чтении из словаря - PullRequest
9 голосов
/ 09 августа 2010

Меня смущает перечисление кода в книге, которую я читаю, C # 3 в двух словах о потоках.В разделе «Безопасность потоков на серверах приложений» в качестве примера пользовательского кэша приведен нижеприведенный код:

static class UserCache
{
    static Dictionary< int,User> _users = new Dictionary< int, User>();

    internal static User GetUser(int id)
    {
        User u = null;

        lock (_users) // Why lock this???
            if (_users.TryGetValue(id, out u))
                return u;

        u = RetrieveUser(id); //Method to retrieve from databse

        lock (_users) _users[id] = u; //Why lock this???
            return u;
    }
}

Авторы объясняют, почему метод RetrieveUser не блокируется, чтобы избежать блокировки кэшана более длительный период.
Меня смущает вопрос, зачем блокировать TryGetValue и обновление словаря, поскольку даже с учетом вышеизложенного словарь обновляется дважды, если два потока вызывают одновременно с одним и тем же невосстановленным идентификатором.

Что достигается блокировкой чтения словаря?
Заранее большое спасибо за все ваши комментарии и идеи.

Ответы [ 4 ]

16 голосов
/ 09 августа 2010

Класс Dictionary<TKey, TValue> не является потокобезопасным .

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

Следовательно, код использует блокировку для предотвращения одновременной записи.

4 голосов
/ 09 августа 2010

Доброкачественное состояние гонки при записи в словарь;как вы указали, два потока могут определить, что в кэше нет совпадающих записей.В этом случае они оба будут читать из БД и затем пытаться вставить.Сохраняется только объект, вставленный последним потоком;другой объект будет собираться мусором, когда с ним завершится первый поток.

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

Обратите внимание, что ConcurrentDictionary, представленный в .NET 4.0, в значительной степени заменяет этот вид идиомы.

1 голос
/ 09 августа 2010

Это обычная практика для доступа к любым не поточно-ориентированным структурам, таким как списки, словари, общие общие значения и т. Д.

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

0 голосов
/ 09 августа 2010

Если два потока вызывают одновременно и идентификатор существует, они оба вернут правильную информацию о пользователе.Первая блокировка - это предотвращение ошибок, как сказал SLaks - если кто-то пишет в словарь, когда вы пытаетесь его прочитать, у вас будут проблемы.В этом случае вторая блокировка никогда не будет достигнута.

Если два потока вызывают одновременно, и идентификатор не существует, один поток заблокирует и введет TryGetValue, это вернет false и установит для u значение по умолчанию,Эта первая блокировка снова, чтобы предотвратить ошибки, описанные SLaks.В этот момент этот первый поток снимет блокировку, а второй поток войдет и сделает то же самое.Затем оба установят для «u» информацию из «RetrieveUser (id)»;это должна быть та же информация.Затем один поток заблокирует словарь и присвоит _users [id] значение u.Эта вторая блокировка такова, что два потока пытаются записать значения в одни и те же области памяти одновременно и портят эту память.Я не знаю, что будет делать второй поток, когда он входит в назначение.Он либо вернет досрочно игнорирующее обновление, либо перезапишет существующие данные из первого потока.В любом случае, словарь будет содержать одну и ту же информацию, поскольку оба потока должны были получить одинаковые данные в 'u' от RetrieveUser.

Для повышения производительности автор сравнил два сценария - приведенный выше сценарий, который будет крайне редким иблокировать, в то время как два потока пытаются записать одни и те же данные, а второй - куда более вероятно, что два потока вызовут запрос данных для объекта, который должен быть записан, и объекта, который существует.Например, threadA и threadB вызывают одновременно, а ThreadA блокирует идентификатор, который не существует.Нет никаких причин заставлять threadB ожидать поиска, пока threadA работает над RetriveUser.Эта ситуация, вероятно, гораздо более вероятна, чем повторяющиеся идентификаторы, описанные выше, поэтому для повышения производительности автор решил не блокировать весь блок.

...