Класс Monitor

Ранее было показано, как реализовать спин-блокировку, используя методы класса Interlocked. Спин-блокировка не всегда является самым эффективным механизмом синхронизации, особенно в среде, где синхронизация почти гарантирована. Планировщик потоков должен будить поток и позволять ему повторно проверять переменную блокировки. Как упоминалось ранее, спин-блокировка идеальна, когда необходим легковесный нереентерабельный механизм, и шансы того, что потоку придется ждать, невелики.

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

обеспечивает синхронизацию, при которой в единицу времени будет выполняться только один метод или блок защищенного кода. Mutex обычно служит для той же цели. Однако Monitor намного легче и быстрее. Monitor подходит, когда требуется защитить доступ к коду внутри одного процесса, a Mutex — когда необходимо защитить доступ к ресурсу из множества процессов.

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

Если вы привыкли к использованию критических секций в Win32, то вам известно, что в некоторой точке должна быть распределена и инициализирована структура CRITICAL_ SECTION.

После этого для входа и выхода из блокировки вызываются Win32-функции EnterCriticalSection и LeaveCriticalSection. Ту же задачу можно решить с использованием класса Monitor в управляемой среде. Для входа и выхода из критической секции вызываются методы Monitor.Enter и Monitor.Exit. Там, где передается объект CRITICALSECTION функциям критической секции Win32, будет передаваться ссылка на объект методам Monitor.

Внутренне CLR управляет блоком синхронизации для каждого экземпляра объекта в процессе. По сути, это флаг того же рода, что и целочисленное значение, которое использовалось в примерах предыдущего раздела, посвященного классу Interlocked. При получении блокировки на объекте флаг устанавливается. Когда блокировка снимается, флаг сбрасывается. — это ворота доступа к этому флагу.

Непостоянство этой схемы проявляется в том, что каждый экземпляр объекта в CLR потенциально содержит одну из таких блокировок. “Потенциально” — так как CLR выделяет их в “ленивом” режиме, поскольку блокировка не каждого объекта будет использована.

Все, что потребуется сделать для реализации критической секции — это создать экземпляр System.Object. Рассмотрим пример применения класса Monitor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
using System;
using System.Threading;
public class EntryPoint
{
static private readonly object theLock = new Object (); 
static private int numberThreads = ecstatic 
private Random rnd = new Random(); 
private static void RndThreadFunc()  {
// Управлять счетчиком потоков и ожидать произвольный промежуток времени от 1 до 12 секунд.
Monitor.Enter ( theLock );
try {
++numberThreads;
finally {
Monitor.Exit ( theLock );
}
int time = rnd.Next ( 1000, 12000 ); 
Thread.Sleep ( time ); 
Monitor.Enter ( theLock ); 
try {
—numberThreads;
}
finally {
Monitor.Exit ( theLock );
}
private static void RptThreadFunc()  { 
while ( true )  {
int threadCount = 0; 
Monitor.Enter ( theLock ); 
try {
threadCount = numberThreads;
}
finally {
Monitor.Exit ( theLock );
}
Console.WriteLine ( "{0} потоков активно", threadCount ); 
Thread.Sleep ( 1000 ) ;
}
static void Main()  {
// Запустить потоки отчетов. 
Thread reporter = new Thread( new ThreadStart (EntryPoint.RptThreadFunc) ); 
reporter.IsBackground = true; 
reporter.Start();
// Запустить потоки, ожидающие в течение случайного периода времени. 
Thread[] rndthreads = new Thread[ 50 ]; 
for( uint i = 0; i < 50; ++i )  { 
rndthreads[i] = new Thread( new ThreadStart (EntryPoint.RndThreadFunc)  ); 
rndthreads[i].Start ();
}

Обратите внимание, что весь доступ к переменной numberThreads производится внутри критической секции в форме объекта блокировки. Перед каждым обращением средство доступа должно получить блокировку на экземпляре объекта theLock. Типом поля theLock является object — просто потому, что действительный тип не имеет значения.

Единственное, что важно — это ссылочный тип, а не тип значения. Поскольку экземпляр object необходим только для того, чтобы использовать его внутренний блок синхронизации, можно просто создать для этого экземпляр System.Object.

Для подстраховки внутренний объект блокировки можно пометить ключевым словом readonly, как это сделано в приведенном выше коде. Это предотвратит непреднамеренное присваивание theLock другого экземпляра, что подвергнет риску всю систему.

Вероятно, вы заметили еще одну вещь — код выглядит более громоздко, чем в версии, использующей методы Interlocked. Для каждого вызова Monitor.Enter должен гарантированно существовать соответствующий ему вызов Monitor.Exit. В примерах применения класса MySpinLock эта проблема смягчается за счет помещения обращения к методам класса Interlocked в класс по имени MySpinLockManager.

Можете ли вы представить себе хаос, который наступит, если вызов Monitor. Exit будет пропущен из-за сгенерированного исключения?

Поэтому в таких ситуациях всегда следует применять блок try/finally. Создатели языка С# осознавали, что разработчикам придется приложить немало усилий, чтобы обеспечить наличие блоков finally во всех случаях, где нужен вызов Monitor .Exit. Они облегчили им жизнь, введя ключевое слово lock. Рассмотрим тот же пример снова, на этот раз применив ключевое слово lock:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;
using System.Threading;
public class EntryPoint
{
static private readonly object theLock = new Object (); 
static private int numberThreads = 0; 
static private Random rnd = new Random(); 
private static void RndThreadFunc()  {
// Управлять счетчиком потоков и ожидать произвольный промежуток времени от 1 до 12 секунд.
lock( theLock ) { 
++numberThreads;
}
int time = rnd.Next ( 1000, 12000 ); 
Thread.Sleep ( time ); 
lock( theLock ) {
—numberThreads;
}
private static void RptThreadFunc ()  { 
while ( true )  {
int threadCount = 0; 
lock( theLock ) {
threadCount = numberThreads;
}
Console.WriteLine ( "{0} поток(ов) активно", threadCount ) ; 
Thread.Sleep ( 1000 ) ;
}
static void Main()  { // Запустить потоки отчетов. 
Thread reporter = new Thread( new ThreadStart (EntryPoint.RptThreadFunc)  ); 
reporter.IsBackground = true; 
reporter.Start() ; // Запустить потоки, ожидающие в течение случайного периода времени. 
Thread[] rndthreads = new Thread[ 50 ]; 
for( uint i = 0; i < 50; ++i )  { 
rndthreads[i] = new Thread( new ThreadStart( EntryPoint.RndThreadFunc) ); 
rndthreads[i].Start();
}

Обратите внимание, что код стал намного яснее, и явные вызовы методов Monitor в нем теперь вообще отсутствуют. “За кулисами” компилятор развертывает ключевое слово lock в знакомую конструкцию try/finally с вызовами Monitor.Enter и Monitor.Exit. В этом легко убедиться, просмотрев сгенерированный код IL с помощью ILDASM.

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

Данный шаблон использования в коде С# встречается часто. Хотя он избавляет от необходимости создания экземпляра объекта System.Object, который весьма легковесен, все же он несет в себе некоторые опасности.

Например, внешний потребитель объекта может попытаться использовать блок синхронизации внутри объекта, передав его экземпляр в Minitor.Enter еще перед вызовом одного из методов, пытающихся захватить ту же блокировку. Формально это разрешено, потому что один и тот же поток может вызывать Monitor.Enetr несколько раз. Другими словами, блокировки Monitor реентерабельны, в отличие от спин-блокировок из предыдущего раздела.

Однако для освобождения блокировки должно быть произведено такое же количество вызовов Monitor.Exit. В этом случае приходится полагаться на потребителя объекта в том, что он применит либо ключевое слово lock, либо блок try/finally, гарантируя, что каждому вызову Monitor.Exit будет соответствовать свой вызов Monitor.Exit.

Когда возможно, старайтесь избегать подобной неизвестности. Использовать блокировку через ключевое слово this не рекомендуется. Вместо этого в качестве объекта блокировки лучше применять приватный экземпляр System.Object. Аналогичного эффекта удалось бы достичь, если бы существовал способ объявить флаг блока синхронизации объекта как private, но, к сожалению, это невозможно.

Вы можете следить за любыми ответами на эту запись через RSS 2.0 ленту. Вы можете оставить ответ, или trackback с вашего собственного сайта.

Оставьте отзыв

XHTML: Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
Rambler's Top100