Почему ConcurrentQueue <T>.Count возвращает 0, когда IsEmpty == true?

Я читал о новых параллельных классах классов в .NET 4 на блог Джеймса Майкла Харе, а страница о ConcurrentQueue<T> говорит:

Тем не менее, по-прежнему рекомендуется, чтобы для пустых проверок вы вызываете IsEmpty вместо сравнения Count с нолем.

Мне любопытно: если есть причина использовать IsEmpty вместо сравнения Count с 0, , почему класс не проверяет IsEmpty и внутренне проверяет значение 0, прежде чем делать какую-либо дорогостоящую работу для подсчета

например:.

public int Count
{
    get
    {
        // Check IsEmpty so we can bail out quicker
        if (this.IsEmpty)
            return 0;

        // Rest of "expensive" counting code
    }
}

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

Ответ 1

Позвольте мне показать пример overstation:

public bool IsEmpty
{
   get { /* always costs 10s here */  }
}

public int Count
{
   get { /* always costs 15s here, no matter Count is 0 or 1 or 2... */  }
}

Если они реализуют свойство Count следующим образом:

public int Count
{
   get
   {
       if (IsEmpty) return 0;
       //original implementation here
   }
}

Теперь, когда окончательный счетчик равен 0, он стоит 10 секунд (на 5 секунд меньше, чем раньше! отлично!), но для тех, кто имеет значение 1/2/больше, он стоит больше, чем раньше, потому что проверка IsEmpty стоит времени! Поэтому оптимизация не является хорошей идеей, потому что IsEmpty требует времени. Хорошо, если IsEmpty читает непосредственно из поля.

EDIT Я проверил реализацию как IsEmpty, так и Count через отражатель, оба из них дороги. Очевидно, что проверка IsEmpty на 0 count только уменьшит среднюю скорость работы.

Ответ 2

ConcurrentQueue<T> является блокировкой и использует ожидания вращения для достижения высокопроизводительного одновременного доступа. Для реализации просто требуется, чтобы была сделана больше работы, чтобы вернуть точное количество, чем проверять, нет ли элементов, поэтому рекомендуется IsEmpty.

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

Я написал простое многопоточное тестовое приложение, которое показало, что Count было на 20% медленнее (с постоянным соперничеством и без конкуренции); однако оба свойства можно назвать миллионы раз в секунду, поэтому любая разница в производительности, вероятно, будет практически ничтожна на практике.

Ответ 3

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

MSDN говорит:

Для определения того, является ли сбор содержит любые предметы, использование этого свойство рекомендуется, а не извлечение количества элементов из свойство Count и сравнение его с 0. Однако, поскольку эта коллекция предназначена для доступа одновременно, может быть, что другой поток изменят коллекцию после IsEmpty возвращает, тем самым аннулируя результат.

Ответ 4

Понимание того, как работают параллельные структуры, очень важно.

if (isEmpty()) ...//do whatever

Если у вас есть параллельная структура, проверка близка к no-op, поскольку все может измениться между isEmpty и любой последующей операцией.

Count повторяется через узлы (не использовал С# почти 6 лет, но аналог java делает то же самое) для вычисления, поэтому это дорогостоящий вызов. Прямой ответ. Проверка isEmpty перед тем, как Count повлечет за собой дополнительную заборку памяти и ничего не добьется. Изменить: если неясно. Подсчитайте, когда очередь пуста стоит точно так же, как isEmpty, однако она стоит много, когда очередь не является!

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