Подтвердить что ты не робот

Правильная блокировка списка <T> в сценариях MultiThreaded?

Хорошо, я просто не могу правильно разобраться в многопоточных сценариях. Извините за повторный аналогичный вопрос, я просто вижу много разных "фактов" в Интернете.

public static class MyClass {
    private static List<string> _myList = new List<string>;
    private static bool _record;

    public static void StartRecording()
    {
        _myList.Clear();
        _record = true;
    }

    public static IEnumerable<string> StopRecording()
    {
        _record = false;
        // Return a Read-Only copy of the list data
        var result = new List<string>(_myList).AsReadOnly();
        _myList.Clear();
        return result;
    }

    public static void DoSomething()
    {
        if(_record) _myList.Add("Test");
        // More, but unrelated actions
    }
}

Идея заключается в том, что если запись активирована, вызовы DoSomething() записываются во внутренний список и возвращаются при вызове StopRecording().

Моя спецификация такова:

  • StartRecording не считается потокобезопасным. Пользователь должен вызывать это, пока другой Thread не вызывает DoSomething(). Но если это так или иначе, это будет здорово.
  • StopRecording также не является официально потокобезопасной. Опять же, было бы здорово, если бы это было так, но это не требование.
  • DoSomething должен быть потокобезопасным

Обычно выглядит так:

    public static void DoSomething()
    {
        object _lock = new object();
        lock(_lock){
            if(_record) _myList.Add("Test");
        }
        // More, but unrelated actions
    }

Альтернативно, объявляя статическую переменную:

    private static object _lock;

    public static void DoSomething()
    {
        lock(_lock){
            if(_record) _myList.Add("Test");
        }
        // More, but unrelated actions
    }

Однако этот ответ говорит, что это не мешает доступу другого кода.

Так что я задаюсь вопросом

  • Как правильно заблокировать список?
  • Должен ли я создать объект блокировки в моей функции или как статическую переменную класса?
  • Могу ли я обернуть функциональность Start и StopRecording в блоке блокировки?
  • StopRecording() делает две вещи: установите логическую переменную в false (чтобы DoSomething() не добавлял больше материала), а затем копирование списка, чтобы вернуть копию данных вызывающему абоненту). Я предполагаю, что _record = false; является атомным и будет действовать немедленно? Поэтому обычно мне не нужно беспокоиться о Multi-Threading вообще, если какой-то другой Thread не вызывает снова StartRecording()?

В конце дня я ищу способ выразить "Хорошо, этот список принадлежит мне сейчас, все остальные потоки должны ждать, пока я не сработаю".

4b9b3361

Ответ 1

Я буду блокировать здесь сам _myList, поскольку он является закрытым, но использование отдельной переменной более распространено. Чтобы улучшить несколько пунктов:

public static class MyClass 
{
    private static List<string> _myList = new List<string>;
    private static bool _record; 

    public static void StartRecording()
    {
        lock(_myList)   // lock on the list
        {
           _myList.Clear();
           _record = true;
        }
    }

    public static IEnumerable<string> StopRecording()
    {
        lock(_myList)
        {
          _record = false;
          // Return a Read-Only copy of the list data
          var result = new List<string>(_myList).AsReadOnly();
          _myList.Clear();
          return result;
        }
    }

    public static void DoSomething()
    {
        lock(_myList)
        {
          if(_record) _myList.Add("Test");
        }
        // More, but unrelated actions
    }
}

Обратите внимание, что этот код использует lock(_myList) для синхронизации доступа к _myList и _record. И вам нужно синхронизировать все действия над этими двумя.

И чтобы согласиться с другими ответами здесь, lock(_myList) ничего не делает для _myList, он просто использует _myList как токен (предположительно как ключ в HashSet). Все методы должны играть честно, запрашивая разрешение, используя тот же токен. Метод другого потока все равно может использовать _myList без блокировки сначала, но с непредсказуемыми результатами.

Мы можем использовать любой токен, поэтому мы часто создаем его специально:

private static object _listLock = new object();

И затем используйте lock(_listLock) вместо lock(_myList) всюду.

Этот метод был бы целесообразным, если myList был общедоступным, и это было бы абсолютно необходимо, если бы вы повторно создали myList вместо вызова Clear().

Ответ 2

Создание новой блокировки в DoSomething(), безусловно, было бы неправильным - было бы бессмысленно, так как каждый вызов DoSomething() использовал бы другую блокировку. Вы должны использовать вторую форму, но с инициализатором:

private static object _lock = new object();

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

Да, вы можете перенести Start/StopRecording в блокировки таким же образом.

Да, установка логической переменной является атомарной, но это не делает ее потокобезопасной. Если вы получаете доступ только к переменной, находящейся в одной и той же блокировке, вы все равно понимаете как атомарность, так и волатильность. В противном случае вы можете увидеть "устаревшие" значения - например. вы устанавливаете значение true в одном потоке, а другой поток может использовать кешированное значение при чтении.

Ответ 3

Существует несколько способов блокировки списка. Вы можете заблокировать функцию _myList напрямую, поскольку _myList никогда не изменяется для ссылки на новый список.

lock (_myList)
{
    // do something with the list...
}

Вы можете создать объект блокировки специально для этой цели.

private static object _syncLock = new object();
lock (_syncLock)
{
    // do something with the list...
}

Если статическая коллекция реализует интерфейс System.Collections.ICollection(List (T)), вы также можете синхронизировать его с помощью свойства SyncRoot.

lock (((ICollection)_myList).SyncRoot)
{
    // do something with the list...
}

Главное, чтобы понять, что вы хотите использовать один и только один объект, который будет использоваться в качестве вашего блокирующего даминала, поэтому создание блокирующего дарителя внутри DoSomething() функция не будет работать. Как сказал Джон, каждый поток, который вызывает DoSomething(), получит свой собственный объект, поэтому блокировка этого объекта будет успешна каждый раз и предоставит немедленный доступ к списку. Стабильный объект блокировки (через сам список, выделенный объект блокировки или свойство ICollection.SyncRoot), он становится общим для всех потоков и может эффективно сериализовать доступ к вашему списку.

Ответ 4

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

lock(_myList)
{
   _myList.Add(...)
}

Ответ 5

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

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

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