В С# было бы лучше использовать Queue.Synchronized или lock() для безопасности потоков?

У меня есть объект Queue, который мне нужно для обеспечения безопасности потоков. Было бы лучше использовать объект блокировки следующим образом:

lock(myLockObject)
{
//do stuff with the queue
}

Или рекомендуется использовать Queue.Synchronized следующим образом:

Queue.Synchronized(myQueue).whatever_i_want_to_do();

Из чтения документов MSDN говорится, что я должен использовать Queue.Synchronized, чтобы сделать его потокобезопасным, но затем он дает пример использования объекта блокировки. Из статьи MSDN:

Чтобы гарантировать безопасность потока Очередь, все операции должны быть выполнены только через эту оболочку.

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

Если вызов Synchronized() не обеспечивает безопасность потоков, что это за точка? Я что-то пропустил?

Ответ 1

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

EDIT: Как отмечено в комментариях, если вы можете использовать абстракции более высокого уровня, это здорово. И если вы используете блокировку, будьте осторожны с ней - документируйте то, что вы ожидаете, чтобы ее заблокировали, и фиксации/освобождения/освобождения за короткий период (больше для корректности, чем производительность). Избегайте вызывать неизвестный код, удерживая блокировку, избегать вложенных блокировок и т.д.

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

Ответ 2

Существует серьезная проблема с методами Synchronized в старой библиотеке коллекции, поскольку они синхронизируются при слишком низком уровне детализации (по методу, а не по единице работы).

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

object item;
if (queue.Count > 0)
{
    // at this point another thread dequeues the last item, and then
    // the next line will throw an InvalidOperationException...
    item = queue.Dequeue();
}

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

object item;
lock (queue)
{
    if (queue.Count > 0)
    {
        item = queue.Dequeue();
    }
}

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

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

Ответ 3

Часто возникает потребность в требованиях к "сборке потоковых сейфов" и требованию выполнить несколько операций над сборкой атомным способом.

Итак, Synchronized() дает вам коллекцию, которая не будет разбиваться, если несколько потоков одновременно добавят к ней элементы, но она не волшебным образом дает вам коллекцию, которая знает, что во время перечисления никто не должен ее трогать.

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

Ответ 4

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

object item;
if (queue.Count > 0)
{
  lock (queue)
  {
    if (queue.Count > 0)
    {
       item = queue.Dequeue();
    }
  }
}

Ответ 5

Мне кажется ясным, что использование блокировки (...) {...} является правильным ответом.

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

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