Какой тип блокировки используется, зависит от того, как к данным получают доступ несколько потоков. Если вы можете точно настроить вариант использования, иногда вы можете полностью исключить необходимость в эксклюзивных блокировках.
Эксклюзивная блокировка необходима только в том случае, если ваш сценарий использования требует, чтобы общие данные всегда были на 100% точными. Это стандарт, с которого большинство разработчиков начинают, потому что именно так мы обычно думаем о данных.
Однако, если то, для чего вы используете данные, может допустить некоторую «разболтанность», есть несколько методов для обмена данными между потоками без использования исключительных блокировок при каждом доступе.
Например, если у вас есть связанный список данных, и если вы используете этот связанный список, вы не будете расстроены, увидев один и тот же узел несколько раз при обходе списка, и не расстроитесь, если он не сразу увидит вставку после вставки (или аналогичных артефактов) вы можете выполнять вставку и удаление списка с помощью обмена атомарными указателями без необходимости полной блокировки мьютекса вокруг операции вставки или удаления.
Другой пример: если у вас есть объект массива или списка, который в основном читается потоками и обновляется только главным потоком, вы можете реализовать обновления без блокировок, поддерживая две копии списка: одну «живую» «что другие потоки могут читать, а другой -« в автономном режиме », в который вы можете писать в частной жизни вашего собственного потока. Чтобы выполнить обновление, вы копируете содержимое «живого» списка в «автономный» список, выполняете обновление в автономный список, а затем заменяете указатель автономного списка на указатель активного списка с помощью обмена атомарными указателями. Затем вам понадобится какой-то механизм, позволяющий читателям «вытекать» из офлайн-списка. В системе сбора мусора вы можете просто освободить ссылку на автономный список - когда с ним покончит последний потребитель, это будет GC'd. В системе без GC вы можете использовать подсчет ссылок, чтобы отслеживать, сколько читателей все еще используют список. Для этого примера было бы идеально иметь только один поток, обозначенный как средство обновления списка. Если требуется несколько средств обновления, вам потребуется установить блокировку вокруг операции обновления, но только для сериализации средств обновления - без блокировки и без влияния на производительность читателей списка.
Все известные мне методы совместного использования ресурсов без блокировки требуют использования атомарных перестановок (или InterlockedExchange). Обычно это приводит к конкретной инструкции в процессоре и / или блокировке аппаратной шины (префикс блокировки для кода операции чтения или записи в ассемблере x86) в течение очень короткого периода времени. В многопроцессорных системах атомные перестановки могут вызвать аннулирование кэша на других процессорах (это имело место в Pentium II с двумя процессорами), но я не думаю, что это является большой проблемой для современных многоядерных чипов. Даже с этими предупреждениями о производительности без блокировок работает намного быстрее, чем с полным событием объекта ядра. Просто вызов функции API ядра занимает несколько сотен тактов (для переключения в режим ядра).
Примеры реальных сценариев:
- рабочие процессы производителя / потребителя. Веб-служба получает запросы http для данных, помещает запрос во внутреннюю очередь, рабочий поток извлекает рабочий элемент из очереди и выполняет работу. Очередь предназначена для чтения / записи и должна быть поточно-ориентированной.
- Данные распределяются между потоками при смене владельца. Поток 1 выделяет объект, бросает его потоку 2 для обработки и никогда больше не хочет его видеть. Поток 2 отвечает за утилизацию объекта. Система управления памятью (malloc / free) должна быть поточно-ориентированной.
- Файловая система. Это почти всегда служба ОС и уже полностью поточно-ориентированная, но ее стоит включить в список.
- Подсчет ссылок. Освобождает ресурс, когда количество ссылок падает до нуля. Операции увеличения / уменьшения / проверки должны быть поточно-ориентированными. Обычно они могут быть реализованы с использованием атомарных примитивов вместо полноценных блокировок Kernel mutex.