Обзор примитивов синхронизации

Платформа .NET Framework предоставляет диапазон примитивов синхронизации для управления взаимодействием потоков и во избежание состояния гонки. Они могут быть условно разделены на три категории: блокировка, сигнал и блокированные операции.

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

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

Обзор включает следующие разделы.

  • Блокировка

  • Сигнализация

  • Типы упрощенной синхронизации

  • Тип SpinWait

  • Блокируемые операции

Блокировка

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

Монопольные блокировки

Самым простым способом блокировки является операция C# lock (SyncLock в Visual Basic), который управляет доступом к блоку кода. Как правило, этот блок называется критическим разделом. Оператор lock реализуется путем использования методов Enter и Exit класса Monitor, он использует try…catch…finally для обеспечения снятия блокировки.

В целом, использование оператора lock для защиты небольших блоков кода, никогда не распространяющееся на несколько методов, всегда является лучшим способом использования класса Monitor. Несмотря на все свои возможности класс Monitor подвержен потерянным блокировкам и взаимоблокировкам.

Класс Monitor

Класс Monitor предоставляет дополнительные функции, которые могут быть использованы вместе с оператором lock:

  • Метод TryEnter позволяет через указанное время разблокировать поток, который заблокирован в ожидании предоставления ресурса. Он возвращает логическое значение, указывающее успешность выполнения или сбой, которое может быть использовано для определения и устранения возможных взаимоблокировок.

  • Метод Wait вызывается потоком в критическом разделе. Он предоставляет управление ресурсом и блокирует поток до того момента, как ресурс снова станет доступным.

  • Методы Pulse и PulseAll разрешают поток, который должен снять блокировку или вызвать метод Wait для помещения одного или нескольких потоков в очередь готовности, чтобы они могли получить блокировку.

Время ожидания для перегрузок метода Wait позволяет ожидающим потокам перейти в очередь готовности.

Класс Monitor может предоставить блокировку в нескольких доменах приложения, если объект использует блокировку, производную из MarshalByRefObject.

Monitor реализует сходство потоков. То есть поток, вошедший в этот класс, должен выйти посредством вызова метода Exit или Wait.

Класс Monitor не поддерживает создание экземпляров. Его методы являются статическими (Shared в Visual Basic) и работают с созданным экземпляром заблокированного объекта.

Обзор концепции см. в разделе Мониторы.

Класс Mutex

Потоки запрашивают Mutex путем вызова перегрузки этого метода WaitOne. Перегрузки со временем ожидания предоставляются для освобождения потоков после определенного времени ожидания. В отличие от класса Monitor мьютекс может быть или локальным, или глобальным. Глобальные мьютексы, также называемые именованными мьютексами, видны в операционной системе и могут использоваться для синхронизации потоков в различных доменах приложения или процессах. Локальные мьютексы происходят из MarshalByRefObject и могут использоваться за границами домена приложения.

Кроме того, Mutex происходит из WaitHandle, что означает возможность его использования в сигнальных механизмах, предоставленных WaitHandle, таким как методы WaitAll, WaitAny и SignalAndWait.

Как и класс Monitor, класс Mutex поддерживает сходство потоков. В отличие от Monitor класс Mutex является объектом, поддерживающим создание экземпляров.

Общие сведения см. в разделе Объекты Mutex.

Класс SpinLock

Начиная с .NET Framework 4, можно использовать класс SpinLock, когда накладные расходы, вызванные использованием Monitor, ухудшают быстродействие. Когда объект SpinLock обнаруживает блокированный критический раздел, он просто входит в цикл, пока блокировка не станет доступной. Если блокировка сохраняется в течение очень короткого времени, вхождение в цикл может обеспечить лучшее быстродействие, чем блокировка. Но если блокировка сохраняется дольше нескольких десятков тактов процессора, SpinLock работает так же хорошо, как и Monitor, но использует больше тактов процессора и, таким образом, может понизить быстродействие других потоков или процессов.

Другие блокировки

Блокировки не должны быть монопольными. Часто бывает полезным разрешить одновременный доступ к ресурсу ограниченному количеству потоков. Семафоры и блокировки чтения и записи предназначены для управления доступом к ресурсу в пуле.

Класс ReaderWriterLock

Класс ReaderWriterLockSlim предназначен для тех случаев, когда поток, изменяющий данные (средство записи) должен иметь монопольный доступ к ресурсу. Если средство записи не активно, любое количество средств чтения может получить доступ к ресурсу (например, посредством вызова метода EnterReadLock). Если поток запрашивает монопольный доступ (например, посредством вызова метода EnterWriteLock), последующие запросы средства чтения блокируются до того момента, как все существующие средства чтения не освободят блокировку, а средство записи не установит и не снимет блокировку.

ReaderWriterLockSlim реализует сходство потоков.

Обзор концепции см. в разделе Блокировки чтения и записи.

Класс Semaphore

Класс Semaphore позволяет доступ к ресурсу определенному количеству потоков. Дополнительные потоки, запрашивающие ресурс, блокируются до освобождения потоком семафора.

Как и класс Mutex, класс Semaphore является производным от класса WaitHandle. Кроме того, как и класс Mutex, класс Semaphore может быть локальным или глобальным. Он может использоваться за пределами границ домена приложения.

В отличие от классов Monitor, Mutex и ReaderWriterLock, класс Semaphore не поддерживает сходство потоков. Это означает, что он может использоваться в сценариях, в которых один поток получает семафор, а другой поток освобождает его.

Общие сведения см. в разделе Классы Semaphore и SemaphoreSlim.

System.Threading.SemaphoreSlim — это упрощенный семафор для синхронизации в границах одного процесса.

К началу

Сигнализация

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

Дескрипторы ожидания предоставляют более полный набор возможностей ожидания и сигнализации.

Дескрипторы ожидания

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

Потоки блокируются при дескрипторах ожидания посредством вызова метода экземпляра WaitOne или одного из статических методов WaitAll, WaitAny или SignalAndWait. Способ их освобождения зависит от того, как метод был вызван, а также от вида дескрипторов блокировки.

Обзор концепции см. в разделе Дескрипторы ожидания.

Дескрипторы ожидания событий

Дескрипторы ожидания событий включают класс EventWaitHandle и его производные классы AutoResetEvent и ManualResetEvent. Потоки освобождаются от дескриптора ожидания событий, если этот дескриптор получает сигнал посредством вызова метода Set или с помощью метода SignalAndWait.

Дескрипторы ожидания событий или сбрасывают себя автоматически, как турникет, разрешающий только один поток при каждом сигнале, или должны быть сброшены вручную, как ворота, которые закрыты до сигнала, а затем открыты, пока их кто-нибудь не закроет. В соответствии с их именами AutoResetEvent и ManualResetEvent представляют, соответственно, первый и второй подходы. System.Threading.ManualResetEventSlim — это упрощенное событие для синхронизации в границах одного процесса.

Дескриптор EventWaitHandle может представлять любой тип события и может быть локальным или глобальным. Производные классы AutoResetEvent и ManualResetEvent всегда являются локальными.

Дескрипторы ожидания событий не поддерживают сходство потоков. Любой поток может подать сигнал дескриптору ожидания событий.

Обзор концепции см. в разделе EventWaitHandle, AutoResetEvent, CountdownEvent и ManualResetEvent.

Классы Mutex и Semaphore

Так как классы Mutex и Semaphore являются производными из класса WaitHandle, они могут использоваться со статическими методами класса WaitHandle. Например, поток может использовать метод WaitAll до ожидания выполнения всех трех условий: подан сигнал дескриптору EventWaitHandle, освобожден мьютекс Mutex и сброшен семафор Semaphore. Так же поток может использовать метод WaitAny для ожидания выполнения одного из этих условий.

Для классов Mutex и Semaphore получение сигнала означает освобождение. Если в качестве первого аргумента метода SignalAndWait используется любой из этих типов, он освобождается. В случае класса Mutex, поддерживающего сходство потоков, исключение создается, если вызывающий поток не имеет собственного мьютекса. Как отмечалось ранее, семафоры не поддерживают сходство потоков.

Барьер

Класс Barrier предоставляет способ циклической синхронизации нескольких потоков, позволяющий заблокировать их все в одной точке и дождаться завершения всех остальных потоков. Барьер полезен, когда одному или нескольким потокам для перехода к следующему этапу алгоритма требуются результаты выполнения другого потока. Дополнительные сведения см. в разделе Барьер (.NET Framework).

К началу

Типы упрощенной синхронизации

Начиная с .NET Framework 4, можно использовать примитивы синхронизации, обеспечивающие высокое быстродействие путем предотвращения, по возможности, ресурсоемкого использования объектов ядра Win32, таких как дескрипторы ожидания. Обычно эти типы следует использовать для коротких времен ожидания и только, проверив исходные типы синхронизации и убедившись в их негодности. Эти упрощенные типы нельзя использовать в сценариях, требующих взаимодействия между процессами.

  • Семафор System.Threading.SemaphoreSlim является упрощенной версией семафора System.Threading.Semaphore.

  • Семафор System.Threading.ManualResetEventSlim является упрощенной версией семафора System.Threading.ManualResetEvent.

  • Класс System.Threading.CountdownEvent представляет собой событие, возникающее, когда его счетчик становится равным нулю.

  • Барьер System.Threading.Barrier позволяет синхронизировать друг с другом несколько потоков без управления со стороны главного потока. Барьер не позволяет каждому из потоков продолжить выполнение, пока все потоки не достигнут заданной точки.

К началу

Тип SpinWait

Начиная с .NET Framework 4, можно использовать структуру System.Threading.SpinWait, когда потоку нужно дождаться возникновения события или выполнения условия, но когда предполагается, что фактическое время ожидания будет меньше требуемого времени ожидания, требуется применить дескриптор ожидания или другой способ блокировки текущего потока. Используя SpinWait, можно задать короткий интервал времени для цикла ожидания, а затем возвратить управление (например, с помощью ожидания или спящего режима), только если условие не было выполнено в течение заданного времени.

К началу

Блокируемые операции

Блокируемые операции — это простые атомарные операции, выполняемые в области памяти статическими методами класса Interlocked. Эти атомарные операции включают добавление, увеличение и уменьшение, обмен, условный обмен в зависимости от сравнения, а также операции чтения для 64-разрядных значений на 32-разрядных платформах.

ПримечаниеПримечание

Гарантия атомарности ограничена отдельными операциями; если необходимо выполнить несколько операций в одном блоке, следует использовать более весомые механизмы синхронизации.

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

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

Общие сведения см. в разделе Блокируемые операции.

К началу

См. также

Основные понятия

Синхронизация данных для многопоточности

Мониторы

Объекты Mutex

Классы Semaphore и SemaphoreSlim

Дескрипторы ожидания

Блокируемые операции

Блокировки чтения и записи

Барьер (.NET Framework)

SpinLock

Другие ресурсы

EventWaitHandle, AutoResetEvent, CountdownEvent и ManualResetEvent

SpinWait