Interlocked.CompareExchange действительно быстрее, чем простая блокировка?
Я наткнулся на ConcurrentDictionary
реализация для.NET 3.5 (извините, я смог найти ссылку прямо сейчас), которая использует этот подход для блокировки:
var current = Thread.CurrentThread.ManagedThreadId;
while (Interlocked.CompareExchange(ref owner, current, 0) != current) { }
// PROCESS SOMETHING HERE
if (current != Interlocked.Exchange(ref owner, 0))
throw new UnauthorizedAccessException("Thread had access to cache even though it shouldn't have.");
Вместо традиционного lock
:
lock(lockObject)
{
// PROCESS SOMETHING HERE
}
Вопрос: есть ли реальная причина для этого? Это быстрее или есть какая-то скрытая выгода?
PS: я знаю, что есть ConcurrentDictionary
в некоторых последних версиях.NET, но я не могу использовать для устаревшего проекта.
Редактировать:
В моем конкретном случае я просто манипулирую внутренним Dictionary
Класс таким образом, что это потокобезопасно.
Пример:
public bool RemoveItem(TKey key)
{
// open lock
var current = Thread.CurrentThread.ManagedThreadId;
while (Interlocked.CompareExchange(ref owner, current, 0) != current) { }
// real processing starts here (entries is a regular `Dictionary` class.
var found = entries.Remove(key);
// verify lock
if (current != Interlocked.Exchange(ref owner, 0))
throw new UnauthorizedAccessException("Thread had access to cache even though it shouldn't have.");
return found;
}
Как и предположил @doctorlove, это код: https://github.com/miensol/SimpleConfigSections/blob/master/SimpleConfigSections/Cache.cs
7 ответов
Ваш пример кода CompareExchange не снимает блокировку, если исключение вызвано "ПРОЦЕСС ЧТО-ТО ЗДЕСЬ".
По этой причине, так же как и по более простому, более читабельному коду, я бы предпочел оператор блокировки.
Вы можете исправить проблему с помощью try/finally, но это делает код еще более уродливым.
Связанный ConcurrentDictionary
Реализация имеет ошибку: она не сможет снять блокировку, если вызывающая сторона пропустит нулевой ключ, потенциально оставляя другие потоки вращающимися бесконечно.
Что касается эффективности, ваша версия CompareExchange по сути является Spinlock, которая может быть эффективной, если потоки могут быть заблокированы только на короткие периоды времени. Но вставка в управляемый словарь может занять относительно много времени, поскольку может потребоваться изменить размер словаря. Поэтому, ИМХО, это не очень хороший кандидат для спин-блокировки, что может быть расточительным, особенно в однопроцессорной системе.
На ваш вопрос нет однозначного ответа. Я бы ответил: это зависит.
Код, который вы предоставили, делает:
- ждать, пока объект окажется в известном состоянии (
threadId == 0 == no current work
) - Работай
- вернуть известное состояние объекта
- другой поток теперь тоже может работать, потому что он может перейти от шага 1 к шагу 2
Как вы уже заметили, в коде есть цикл, который фактически выполняет этап ожидания. Вы не блокируете поток, пока не получите доступ к критическому разделу, вместо этого вы просто используете процессор. Попробуйте заменить вашу обработку (в вашем случае, звонок в Remove
) от Thread.Sleep(2000)
вы увидите другой "ожидающий" поток, потребляющий все один из ваших процессоров в течение 2 с в цикле.
Что означает, какой из них лучше, зависит от нескольких факторов. Например: сколько существует одновременных обращений? Сколько времени занимает операция? Сколько у вас процессоров?
я хотел бы использовать lock
вместо Interlocked
потому что это легче читать и поддерживать. Исключение составляют случаи, когда у вас есть фрагмент кода, который называется миллионы раз, и конкретный вариант использования, в котором вы уверены. Interlocked
быстрее.
Таким образом, вам придется самостоятельно измерить оба подхода. Если у вас нет времени для этого, то вам, вероятно, не нужно беспокоиться о выступлениях, и вы должны использовать lock
,
Немного поздно... Я прочитал ваш пример, но вкратце:
Самая медленная синхронизация MT:
- Interlocked. * => Это атомарная инструкция процессора. Не может быть побежден, если этого достаточно для вашей необходимости.
- SpinLock => Использует блокировку сзади и очень быстро. Использует процессор при ожидании. Не используйте код, ожидающий долгое время (обычно он используется для предотвращения переключения потоков для блокировки, которая выполняет быстрые действия). Если вам часто приходится ждать более одного потока, я бы предложил использовать "Блокировку"
- Lock => Самый медленный, но простой в использовании и чтении, чем SpinLock. Сама инструкция очень быстрая, но если она не может получить блокировку, она откажется от процессора. За кулисами он будет выполнять WaitForSingleObject в объекте ядра (CriticalSection), а затем Window будет предоставлять процессору время процессора только тогда, когда блокировка будет освобождена потоком, который его получил.
Веселитесь вместе с МТ!
Да. Interlocked
Класс предлагает атомарные операции, что означает, что они не блокируют другой код, как блокировка, потому что им это не нужно. Когда вы блокируете блок кода, вы хотите убедиться, что в нем нет двух потоков одновременно, это означает, что когда поток находится внутри, все остальные потоки ожидают входа, который использует ресурсы (время процессора и незанятые потоки). С другой стороны, атомарные операции не должны блокировать другие атомарные операции, потому что они атомарные. Концептуально это операция с одним ЦП, последующие просто идут после предыдущей, и вы не тратите потоки на простое ожидание. (Кстати, именно поэтому он ограничен очень простыми операциями, такими как Increment
, Exchange
так далее.)
Я думаю, что блокировка (под которой находится монитор) использует блокировку, чтобы узнать, снята ли блокировка, но она не может знать, что действия внутри нее могут быть атомарными.
В большинстве случаев, однако, разница не является критической. Но вы должны проверить это для вашего конкретного случая.
Документы для Interlocked
класс скажи нам это
"Предоставляет элементарные операции для переменных, которые используются несколькими потоками".
Теория атомарной операции может быть быстрее, чем блокировки. Албахари дает некоторые дополнительные сведения о взаимосвязанных операциях, утверждая, что они быстрее.
Обратите внимание, что Interlocked
обеспечивает "меньший" интерфейс, чем Lock
- см. предыдущий вопрос здесь
Одно важное различие между блокировкой и interlock.CompareExhange заключается в том, как его можно использовать в асинхронных средах.
async-операции не могут ожидаться внутри блокировки, так как они могут легко возникнуть в тупиковых ситуациях, если поток, продолжающий выполнение после ожидания, не тот, который изначально получил блокировку.
Однако это не проблема с блокировкой, потому что поток ничего не «получает».
Другим решением для асинхронного кода, которое может обеспечить лучшую читаемость, чем заблокированный, может быть семафор, как описано в этом сообщении в блоге:https://blog.cdemi.io/async-waiting-inside-c-sharp-locks/
Interlocked работает быстрее - уже объяснено в других комментариях, и вы также можете определить логику реализации ожидания, например, spinWait.spin(), spinUntil, Thread.sleep и т. Д. После сбоя блокировки в первый раз.. Кроме того, если ваш код в ожидается, что блокировка будет работать без возможности сбоя (пользовательский код / делегаты / разрешение ресурсов или выделение / события / неожиданный код, выполняемый во время блокировки), если вы не собираетесь перехватывать исключение, чтобы позволить вашему программному обеспечению продолжить выполнение, "попробуйте" "finally" также пропускается, поэтому дополнительная скорость отсутствует. lock(something) гарантирует, что если вы поймаете исключение извне, чтобы разблокировать это что-то, точно так же, как "using" гарантирует (C#), когда выполнение завершает блок выполнения по какой-либо причине, чтобы удалить "используемый" одноразовый объект.