Класс System.Threading.Monitor

В этой статье приводятся дополнительные замечания к справочной документации по этому API.

Класс Monitor позволяет синхронизировать доступ к региону кода, принимая и выпуская блокировку для определенного объекта, вызывая Monitor.EnterMonitor.TryEnterметоды и Monitor.Exit методы. Блокировки объектов предоставляют возможность ограничить доступ к блоку кода, который обычно называется критическим разделом. Хотя поток владеет блокировкой для объекта, ни один другой поток не может получить этот блок. Вы также можете использовать Monitor класс, чтобы убедиться, что другой поток не может получить доступ к разделу кода приложения, выполняемого владельцем блокировки, если другой поток не выполняет код с помощью другого заблокированного объекта. Так как класс Monitor имеет сходство потоков, поток, полученный блокировкой, должен освободить блокировку, вызвав метод Monitor.Exit.

Обзор

Monitor имеет следующие функции:

  • Он связан с объектом по запросу.
  • Это означает, что он может вызываться непосредственно из любого контекста.
  • Невозможно создать экземпляр Monitor класса. Методы Monitor класса являются статическими. Каждый метод передает синхронизированный объект, который управляет доступом к критическому разделу.

Примечание.

Monitor Используйте класс для блокировки объектов, отличных от строк (т. е. ссылочных типов, отличных Stringот типов значений). Дополнительные сведения см. в разделе о перегрузках Enter метода и разделе объекта блокировки далее в этой статье.

В следующей таблице описываются действия, которые могут выполняться потоками, которые обращаются к синхронизированным объектам:

Действие Description
Enter, TryEnter Получает блокировку для объекта. Это действие также знаменует собой начало критического раздела. Ни один другой поток не может ввести критически важный раздел, если он не выполняет инструкции в критическом разделе с использованием другого заблокированного объекта.
Wait Освобождает блокировку объекта, чтобы разрешить другим потокам блокировать и получать доступ к объекту. Вызывающий поток ожидает, пока другой поток обращается к объекту. Сигналы пульса используются для уведомления о изменениях состояния объекта в ожидании потоков.
Pulse (сигнал), PulseAll Отправляет сигнал одному или нескольким потокам ожидания. Сигнал уведомляет поток ожидания о том, что состояние заблокированного объекта изменилось, и владелец блокировки готов освободить блокировку. Поток ожидания помещается в очередь готовности объекта, чтобы в конечном итоге он мог получить блокировку для объекта. После блокировки поток может проверка новое состояние объекта, чтобы узнать, достигнуто ли требуемое состояние.
Exit Освобождает блокировку объекта. Это действие также помечает конец критического раздела, защищенного заблокированным объектом.

Существует два набора перегрузок для Enter методов и TryEnter методов. Один набор перегрузки имеет ref параметр (в C#) или ByRef (в Visual Basic), Boolean который атомарно устанавливается true , если блокировка получена, даже если при получении блокировки возникает исключение. Используйте эти перегрузки, если крайне важно освободить блокировку во всех случаях, даже если ресурсы, защищенные блокировкой, могут не находиться в согласованном состоянии.

Объект блокировки

Класс Monitor состоит из static методов (Shared в Visual Basic), которые работают с объектом, который управляет доступом к критическому разделу. Следующие сведения поддерживаются для каждого синхронизированного объекта:

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

Monitor блокирует объекты (то есть ссылочные типы), а не типы значений. Хотя можно передать тип значения в Enter и Exit, он упаковывается отдельно для каждого вызова. Поскольку при каждом вызове создается отдельный объект, Enter никогда не выполняет блокировку, а код, который он предположительно защищает, на самом деле не синхронизируется. Кроме того, объект, переданный в Exit, отличается от объекта, переданного в Enter, поэтому Monitor вызывает исключение SynchronizationLockException с сообщением «Для не синхронизированного блока кода вызван метод синхронизации объектов».

Приведенный ниже пример иллюстрирует данную проблему. Он запускает десять задач, каждая из которых просто бездействует в течение 250 миллисекунд. Затем каждая задача обновляет переменную счетчика nTasks, который предназначен для подсчета количества фактически запущенных и выполненных задач. Поскольку nTasks является глобальной переменной, которая может обновляться несколькими задачами одновременно, используется монитор, защищающий ее от одновременного изменения несколькими задачами. Тем не менее, как показывают выходные данные в примере, каждая из задач вызывает исключение SynchronizationLockException.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

public class Example1
{
    public static void Main()
    {
        int nTasks = 0;
        List<Task> tasks = new List<Task>();

        try
        {
            for (int ctr = 0; ctr < 10; ctr++)
                tasks.Add(Task.Run(() =>
                { // Instead of doing some work, just sleep.
                    Thread.Sleep(250);
                    // Increment the number of tasks.
                    Monitor.Enter(nTasks);
                    try
                    {
                        nTasks += 1;
                    }
                    finally
                    {
                        Monitor.Exit(nTasks);
                    }
                }));
            Task.WaitAll(tasks.ToArray());
            Console.WriteLine("{0} tasks started and executed.", nTasks);
        }
        catch (AggregateException e)
        {
            String msg = String.Empty;
            foreach (var ie in e.InnerExceptions)
            {
                Console.WriteLine("{0}", ie.GetType().Name);
                if (!msg.Contains(ie.Message))
                    msg += ie.Message + Environment.NewLine;
            }
            Console.WriteLine("\nException Message(s):");
            Console.WriteLine(msg);
        }
    }
}
// The example displays the following output:
//    SynchronizationLockException
//    SynchronizationLockException
//    SynchronizationLockException
//    SynchronizationLockException
//    SynchronizationLockException
//    SynchronizationLockException
//    SynchronizationLockException
//    SynchronizationLockException
//    SynchronizationLockException
//    SynchronizationLockException
//
//    Exception Message(s):
//    Object synchronization method was called from an unsynchronized block of code.
Imports System.Collections.Generic
Imports System.Threading
Imports System.Threading.Tasks

Module Example3
    Public Sub Main()
        Dim nTasks As Integer = 0
        Dim tasks As New List(Of Task)()

        Try
            For ctr As Integer = 0 To 9
                tasks.Add(Task.Run(Sub()
                                       ' Instead of doing some work, just sleep.
                                       Thread.Sleep(250)
                                       ' Increment the number of tasks.
                                       Monitor.Enter(nTasks)
                                       Try
                                           nTasks += 1
                                       Finally
                                           Monitor.Exit(nTasks)
                                       End Try
                                   End Sub))
            Next
            Task.WaitAll(tasks.ToArray())
            Console.WriteLine("{0} tasks started and executed.", nTasks)
        Catch e As AggregateException
            Dim msg As String = String.Empty
            For Each ie In e.InnerExceptions
                Console.WriteLine("{0}", ie.GetType().Name)
                If Not msg.Contains(ie.Message) Then
                    msg += ie.Message + Environment.NewLine
                End If
            Next
            Console.WriteLine(vbCrLf + "Exception Message(s):")
            Console.WriteLine(msg)
        End Try
    End Sub
End Module
' The example displays the following output:
'    SynchronizationLockException
'    SynchronizationLockException
'    SynchronizationLockException
'    SynchronizationLockException
'    SynchronizationLockException
'    SynchronizationLockException
'    SynchronizationLockException
'    SynchronizationLockException
'    SynchronizationLockException
'    SynchronizationLockException
'
'    Exception Message(s):
'    Object synchronization method was called from an unsynchronized block of code.

Каждая задача вызывает исключение SynchronizationLockException из-за того, что переменная nTasks упаковывается перед вызовом метода Monitor.Enter в каждой задаче. Другими словами, в каждый вызов метода передается отдельная переменная, которая независима от остальных. nTasks снова упаковывается в вызове метода Monitor.Exit. И снова при этом создается десять новых упакованных переменных, которые не зависят друг от друга, nTasks, и десять упакованных переменных, созданных при вызове метода Monitor.Enter. Затем вызывается исключение, поскольку наш код пытается снять блокировку для вновь созданной переменной, которая ранее не была заблокирована.

Хотя можно упаковать переменную типа значения перед вызовом Enter и Exit, как показано в следующем примере, и передать тот же упакованный объект в оба метода, такой подход не дает никаких преимуществ. Изменения неупакованной переменной не отражаются в упакованной копии, и возможность изменения значения упакованной копии отсутствует.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {

      int nTasks = 0;
      object o = nTasks;
      List<Task> tasks = new List<Task>();

      try {
         for (int ctr = 0; ctr < 10; ctr++)
            tasks.Add(Task.Run( () => { // Instead of doing some work, just sleep.
                                        Thread.Sleep(250);
                                        // Increment the number of tasks.
                                        Monitor.Enter(o);
                                        try {
                                           nTasks++;
                                        }
                                        finally {
                                           Monitor.Exit(o);
                                        }
                                      } ));
         Task.WaitAll(tasks.ToArray());
         Console.WriteLine("{0} tasks started and executed.", nTasks);
      }
      catch (AggregateException e) {
         String msg = String.Empty;
         foreach (var ie in e.InnerExceptions) {
            Console.WriteLine("{0}", ie.GetType().Name);
            if (! msg.Contains(ie.Message))
               msg += ie.Message + Environment.NewLine;
         }
         Console.WriteLine("\nException Message(s):");
         Console.WriteLine(msg);
      }
   }
}
// The example displays the following output:
//        10 tasks started and executed.
Imports System.Collections.Generic
Imports System.Threading
Imports System.Threading.Tasks

Module Example2
    Public Sub Main()
        Dim nTasks As Integer = 0
        Dim o As Object = nTasks
        Dim tasks As New List(Of Task)()

        Try
            For ctr As Integer = 0 To 9
                tasks.Add(Task.Run(Sub()
                                       ' Instead of doing some work, just sleep.
                                       Thread.Sleep(250)
                                       ' Increment the number of tasks.
                                       Monitor.Enter(o)
                                       Try
                                           nTasks += 1
                                       Finally
                                           Monitor.Exit(o)
                                       End Try
                                   End Sub))
            Next
            Task.WaitAll(tasks.ToArray())
            Console.WriteLine("{0} tasks started and executed.", nTasks)
        Catch e As AggregateException
            Dim msg As String = String.Empty
            For Each ie In e.InnerExceptions
                Console.WriteLine("{0}", ie.GetType().Name)
                If Not msg.Contains(ie.Message) Then
                    msg += ie.Message + Environment.NewLine
                End If
            Next
            Console.WriteLine(vbCrLf + "Exception Message(s):")
            Console.WriteLine(msg)
        End Try
    End Sub
End Module
' The example displays the following output:
'       10 tasks started and executed.

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

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

Критический раздел

EnterExit Используйте методы, чтобы пометить начало и конец критического раздела.

Примечание.

Функциональные возможности, предоставляемые Enter методами, Exit идентичными инструкции блокировки в C# и инструкции SyncLock в Visual Basic, за исключением того, что язык создает Monitor.Enter(Object, Boolean) перегрузку метода и Monitor.Exit метод в ...tryfinally блокировать, чтобы монитор был освобожден.

Если критически важный раздел является набором смежных инструкций, блокировка, полученная Enter методом, гарантирует, что только один поток может выполнить заключенный код с заблокированным объектом. В этом случае рекомендуется поместить этот код в try блок и поместить вызов Exit метода в finally блок. Это гарантирует снятие блокировки даже при возникновении исключения. Следующий фрагмент кода иллюстрирует этот шаблон.

// Define the lock object.
var obj = new Object();

// Define the critical section.
Monitor.Enter(obj);
try
{
    // Code to execute one thread at a time.
}
// catch blocks go here.
finally
{
    Monitor.Exit(obj);
}
' Define the lock object.
Dim obj As New Object()

' Define the critical section.
Monitor.Enter(obj)
Try
    ' Code to execute one thread at a time.

    ' catch blocks go here.
Finally
    Monitor.Exit(obj)
End Try

Обычно это средство используется для синхронизации доступа к статическому или экземплярному методу класса.

Если критически важный раздел охватывает весь метод, средство блокировки может быть достигнуто путем размещения System.Runtime.CompilerServices.MethodImplAttribute метода и указания Synchronized значения в конструкторе System.Runtime.CompilerServices.MethodImplAttribute. При использовании этого атрибута Enter вызовы методов Exit не требуются. Следующий фрагмент кода иллюстрирует этот шаблон:

[MethodImplAttribute(MethodImplOptions.Synchronized)]
void MethodToLock()
{
    // Method implementation.
}
<MethodImplAttribute(MethodImplOptions.Synchronized)>
Sub MethodToLock()
    ' Method implementation.
End Sub

Обратите внимание, что атрибут приводит к тому, что текущий поток удерживает блокировку, пока метод не возвращается; Если блокировка может быть выпущена раньше, используйте Monitor класс, оператор блокировки C# или инструкцию Visual Basic SyncLock внутри метода вместо атрибута.

Хотя это возможно для Enter операторов Exit , которые блокируют и освобождают заданный объект для пересечения границ членов или классов или обоих, эта практика не рекомендуется.

Пульс, ПульсAll и ожидание

После того как поток владеет блокировкой и вошел в критически важный раздел, который защищает блокировка, он может вызывать Monitor.Waitметоды Monitor.Pulseи Monitor.PulseAll методы.

Когда поток, содержащий вызовы Waitблокировки, освобождается блокировка и поток добавляется в очередь ожидания синхронизированного объекта. Первый поток в очереди готовности, если таковой имеется, получает блокировку и вводит критически важный раздел. Вызываемый Wait поток перемещается из очереди ожидания в готовую очередь, когда Monitor.PulseMonitor.PulseAll вызывается потоком, который содержит блокировку (для перемещения поток должен находиться в голове очереди ожидания). Метод Wait возвращается, когда вызывающий поток повторно запрашивает блокировку.

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

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

Важно отметить различие между использованием Monitor класса и WaitHandle объектов.

  • Класс Monitor является исключительно управляемым, полностью переносимым и может быть более эффективным с точки зрения требований к ресурсам операционной системы.
  • Объекты WaitHandle представляют объекты ожидания операционной системы, удобны для синхронизации между управляемым и неуправляемым кодом и предоставляют некоторые расширенные функции операционной системы, например возможность ожидания сразу нескольких объектов.

Примеры

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

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

public class Example2
{
    public static void Main()
    {
        List<Task> tasks = new List<Task>();
        Random rnd = new Random();
        long total = 0;
        int n = 0;

        for (int taskCtr = 0; taskCtr < 10; taskCtr++)
            tasks.Add(Task.Run(() =>
            {
                int[] values = new int[10000];
                int taskTotal = 0;
                int taskN = 0;
                int ctr = 0;
                Monitor.Enter(rnd);
                // Generate 10,000 random integers
                for (ctr = 0; ctr < 10000; ctr++)
                    values[ctr] = rnd.Next(0, 1001);
                Monitor.Exit(rnd);
                taskN = ctr;
                foreach (var value in values)
                    taskTotal += value;

                Console.WriteLine("Mean for task {0,2}: {1:N2} (N={2:N0})",
                                  Task.CurrentId, (taskTotal * 1.0) / taskN,
                                  taskN);
                Interlocked.Add(ref n, taskN);
                Interlocked.Add(ref total, taskTotal);
            }));
        try
        {
            Task.WaitAll(tasks.ToArray());
            Console.WriteLine("\nMean for all tasks: {0:N2} (N={1:N0})",
                              (total * 1.0) / n, n);
        }
        catch (AggregateException e)
        {
            foreach (var ie in e.InnerExceptions)
                Console.WriteLine("{0}: {1}", ie.GetType().Name, ie.Message);
        }
    }
}
// The example displays output like the following:
//       Mean for task  1: 499.04 (N=10,000)
//       Mean for task  2: 500.42 (N=10,000)
//       Mean for task  3: 499.65 (N=10,000)
//       Mean for task  8: 502.59 (N=10,000)
//       Mean for task  5: 502.75 (N=10,000)
//       Mean for task  4: 494.88 (N=10,000)
//       Mean for task  7: 499.22 (N=10,000)
//       Mean for task 10: 496.45 (N=10,000)
//       Mean for task  6: 499.75 (N=10,000)
//       Mean for task  9: 502.79 (N=10,000)
//
//       Mean for all tasks: 499.75 (N=100,000)
Imports System.Collections.Generic
Imports System.Threading
Imports System.Threading.Tasks

Module Example4
    Public Sub Main()
        Dim tasks As New List(Of Task)()
        Dim rnd As New Random()
        Dim total As Long = 0
        Dim n As Integer = 0

        For taskCtr As Integer = 0 To 9
            tasks.Add(Task.Run(Sub()
                                   Dim values(9999) As Integer
                                   Dim taskTotal As Integer = 0
                                   Dim taskN As Integer = 0
                                   Dim ctr As Integer = 0
                                   Monitor.Enter(rnd)
                                   ' Generate 10,000 random integers.
                                   For ctr = 0 To 9999
                                       values(ctr) = rnd.Next(0, 1001)
                                   Next
                                   Monitor.Exit(rnd)
                                   taskN = ctr
                                   For Each value In values
                                       taskTotal += value
                                   Next

                                   Console.WriteLine("Mean for task {0,2}: {1:N2} (N={2:N0})",
                                                  Task.CurrentId, taskTotal / taskN,
                                                  taskN)
                                   Interlocked.Add(n, taskN)
                                   Interlocked.Add(total, taskTotal)
                               End Sub))
        Next

        Try
            Task.WaitAll(tasks.ToArray())
            Console.WriteLine()
            Console.WriteLine("Mean for all tasks: {0:N2} (N={1:N0})",
                           (total * 1.0) / n, n)
        Catch e As AggregateException
            For Each ie In e.InnerExceptions
                Console.WriteLine("{0}: {1}", ie.GetType().Name, ie.Message)
            Next
        End Try
    End Sub
End Module
' The example displays output like the following:
'       Mean for task  1: 499.04 (N=10,000)
'       Mean for task  2: 500.42 (N=10,000)
'       Mean for task  3: 499.65 (N=10,000)
'       Mean for task  8: 502.59 (N=10,000)
'       Mean for task  5: 502.75 (N=10,000)
'       Mean for task  4: 494.88 (N=10,000)
'       Mean for task  7: 499.22 (N=10,000)
'       Mean for task 10: 496.45 (N=10,000)
'       Mean for task  6: 499.75 (N=10,000)
'       Mean for task  9: 502.79 (N=10,000)
'
'       Mean for all tasks: 499.75 (N=100,000)

Так как к ней можно получить доступ из любой задачи, выполняемой в потоке пула потоков, доступ к переменным total и n также должен быть синхронизирован. Этот Interlocked.Add метод используется для этой цели.

В следующем примере демонстрируется объединенное использование Monitor класса (реализованного с lock помощью конструкции или SyncLock языка), Interlocked класса и AutoResetEvent класса. Он определяет два класса internal (в C#) или Friend (в Visual Basic), SyncResource и UnSyncResource, которые соответственно предоставляют синхронизированный и несинхронизированный доступ к ресурсу. Чтобы обеспечить демонстрацию в примере различия между синхронизированным и несинхронизированным доступом (что может случиться, если каждый вызов метода завершается быстро), метод включает случайную задержку: для потоков, свойство Thread.ManagedThreadId которых имеет четное значение, метод вызывает Thread.Sleep для введения задержки в 2 000 миллисекунд. Обратите внимание, что поскольку класс SyncResource не является общим, ни один клиентский код не выполняет блокировку в синхронизированном ресурсе; внутренний класс сам выполняет блокировку. Это предотвращает блокировка общедоступного объекта вредоносным кодом.

using System;
using System.Threading;

internal class SyncResource
{
    // Use a monitor to enforce synchronization.
    public void Access()
    {
        lock(this) {
            Console.WriteLine("Starting synchronized resource access on thread #{0}",
                              Thread.CurrentThread.ManagedThreadId);
            if (Thread.CurrentThread.ManagedThreadId % 2 == 0)
                Thread.Sleep(2000);

            Thread.Sleep(200);
            Console.WriteLine("Stopping synchronized resource access on thread #{0}",
                              Thread.CurrentThread.ManagedThreadId);
        }
    }
}

internal class UnSyncResource
{
    // Do not enforce synchronization.
    public void Access()
    {
        Console.WriteLine("Starting unsynchronized resource access on Thread #{0}",
                          Thread.CurrentThread.ManagedThreadId);
        if (Thread.CurrentThread.ManagedThreadId % 2 == 0)
            Thread.Sleep(2000);

        Thread.Sleep(200);
        Console.WriteLine("Stopping unsynchronized resource access on thread #{0}",
                          Thread.CurrentThread.ManagedThreadId);
    }
}

public class App
{
    private static int numOps;
    private static AutoResetEvent opsAreDone = new AutoResetEvent(false);
    private static SyncResource SyncRes = new SyncResource();
    private static UnSyncResource UnSyncRes = new UnSyncResource();

   public static void Main()
   {
        // Set the number of synchronized calls.
        numOps = 5;
        for (int ctr = 0; ctr <= 4; ctr++)
            ThreadPool.QueueUserWorkItem(new WaitCallback(SyncUpdateResource));

        // Wait until this WaitHandle is signaled.
        opsAreDone.WaitOne();
        Console.WriteLine("\t\nAll synchronized operations have completed.\n");

        // Reset the count for unsynchronized calls.
        numOps = 5;
        for (int ctr = 0; ctr <= 4; ctr++)
            ThreadPool.QueueUserWorkItem(new WaitCallback(UnSyncUpdateResource));

        // Wait until this WaitHandle is signaled.
        opsAreDone.WaitOne();
        Console.WriteLine("\t\nAll unsynchronized thread operations have completed.\n");
   }

    static void SyncUpdateResource(Object state)
    {
        // Call the internal synchronized method.
        SyncRes.Access();

        // Ensure that only one thread can decrement the counter at a time.
        if (Interlocked.Decrement(ref numOps) == 0)
            // Announce to Main that in fact all thread calls are done.
            opsAreDone.Set();
    }

    static void UnSyncUpdateResource(Object state)
    {
        // Call the unsynchronized method.
        UnSyncRes.Access();

        // Ensure that only one thread can decrement the counter at a time.
        if (Interlocked.Decrement(ref numOps) == 0)
            // Announce to Main that in fact all thread calls are done.
            opsAreDone.Set();
    }
}
// The example displays output like the following:
//    Starting synchronized resource access on thread #6
//    Stopping synchronized resource access on thread #6
//    Starting synchronized resource access on thread #7
//    Stopping synchronized resource access on thread #7
//    Starting synchronized resource access on thread #3
//    Stopping synchronized resource access on thread #3
//    Starting synchronized resource access on thread #4
//    Stopping synchronized resource access on thread #4
//    Starting synchronized resource access on thread #5
//    Stopping synchronized resource access on thread #5
//
//    All synchronized operations have completed.
//
//    Starting unsynchronized resource access on Thread #7
//    Starting unsynchronized resource access on Thread #9
//    Starting unsynchronized resource access on Thread #10
//    Starting unsynchronized resource access on Thread #6
//    Starting unsynchronized resource access on Thread #3
//    Stopping unsynchronized resource access on thread #7
//    Stopping unsynchronized resource access on thread #9
//    Stopping unsynchronized resource access on thread #3
//    Stopping unsynchronized resource access on thread #10
//    Stopping unsynchronized resource access on thread #6
//
//    All unsynchronized thread operations have completed.
Imports System.Threading

Friend Class SyncResource
    ' Use a monitor to enforce synchronization.
    Public Sub Access()
        SyncLock Me
            Console.WriteLine("Starting synchronized resource access on thread #{0}",
                              Thread.CurrentThread.ManagedThreadId)
            If Thread.CurrentThread.ManagedThreadId Mod 2 = 0 Then
                Thread.Sleep(2000)
            End If
            Thread.Sleep(200)
            Console.WriteLine("Stopping synchronized resource access on thread #{0}",
                              Thread.CurrentThread.ManagedThreadId)
        End SyncLock
    End Sub
End Class

Friend Class UnSyncResource
    ' Do not enforce synchronization.
    Public Sub Access()
        Console.WriteLine("Starting unsynchronized resource access on Thread #{0}",
                          Thread.CurrentThread.ManagedThreadId)
        If Thread.CurrentThread.ManagedThreadId Mod 2 = 0 Then
            Thread.Sleep(2000)
        End If
        Thread.Sleep(200)
        Console.WriteLine("Stopping unsynchronized resource access on thread #{0}",
                          Thread.CurrentThread.ManagedThreadId)
    End Sub
End Class

Public Module App
    Private numOps As Integer
    Private opsAreDone As New AutoResetEvent(False)
    Private SyncRes As New SyncResource()
    Private UnSyncRes As New UnSyncResource()

    Public Sub Main()
        ' Set the number of synchronized calls.
        numOps = 5
        For ctr As Integer = 0 To 4
            ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf SyncUpdateResource))
        Next
        ' Wait until this WaitHandle is signaled.
        opsAreDone.WaitOne()
        Console.WriteLine(vbTab + Environment.NewLine + "All synchronized operations have completed.")
        Console.WriteLine()

        numOps = 5
        ' Reset the count for unsynchronized calls.
        For ctr As Integer = 0 To 4
            ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf UnSyncUpdateResource))
        Next

        ' Wait until this WaitHandle is signaled.
        opsAreDone.WaitOne()
        Console.WriteLine(vbTab + Environment.NewLine + "All unsynchronized thread operations have completed.")
    End Sub

    Sub SyncUpdateResource()
        ' Call the internal synchronized method.
        SyncRes.Access()

        ' Ensure that only one thread can decrement the counter at a time.
        If Interlocked.Decrement(numOps) = 0 Then
            ' Announce to Main that in fact all thread calls are done.
            opsAreDone.Set()
        End If
    End Sub

    Sub UnSyncUpdateResource()
        ' Call the unsynchronized method.
        UnSyncRes.Access()

        ' Ensure that only one thread can decrement the counter at a time.
        If Interlocked.Decrement(numOps) = 0 Then
            ' Announce to Main that in fact all thread calls are done.
            opsAreDone.Set()
        End If
    End Sub
End Module
' The example displays output like the following:
'    Starting synchronized resource access on thread #6
'    Stopping synchronized resource access on thread #6
'    Starting synchronized resource access on thread #7
'    Stopping synchronized resource access on thread #7
'    Starting synchronized resource access on thread #3
'    Stopping synchronized resource access on thread #3
'    Starting synchronized resource access on thread #4
'    Stopping synchronized resource access on thread #4
'    Starting synchronized resource access on thread #5
'    Stopping synchronized resource access on thread #5
'
'    All synchronized operations have completed.
'
'    Starting unsynchronized resource access on Thread #7
'    Starting unsynchronized resource access on Thread #9
'    Starting unsynchronized resource access on Thread #10
'    Starting unsynchronized resource access on Thread #6
'    Starting unsynchronized resource access on Thread #3
'    Stopping unsynchronized resource access on thread #7
'    Stopping unsynchronized resource access on thread #9
'    Stopping unsynchronized resource access on thread #3
'    Stopping unsynchronized resource access on thread #10
'    Stopping unsynchronized resource access on thread #6
'
'    All unsynchronized thread operations have completed.

В примере определяется переменная numOps, задающая число потоков, которые будут пытаться получить доступ к ресурсу. Поток приложения вызывает метод ThreadPool.QueueUserWorkItem(WaitCallback) для синхронизированного и несинхронизированного доступа по пять раз. Метод ThreadPool.QueueUserWorkItem(WaitCallback) имеет единственный параметр, делегат, который не принимает никаких параметров и не возвращает значений. Для синхронизированного доступа он вызывает метод SyncUpdateResource; для несинхронизированного доступа он вызывает метод UnSyncUpdateResource. После каждого набора вызовов метода поток приложения вызывает метод AutoResetEvent.WaitOne , чтобы он блокировался до сигнала экземпляра AutoResetEvent .

Каждый вызов метода SyncUpdateResource вызывает внутренний метод SyncResource.Access, а затем вызывает метод Interlocked.Decrement для уменьшения счетчика numOps. Метод Interlocked.Decrement используется для уменьшения счетчика, так как в противном случае невозможно убедиться, что второй поток получит доступ к значению до того, как в переменной сохранено значение первого потока. Когда последний синхронизированный рабочий поток уменьшает счетчик до нуля, указывая, что все синхронизированные потоки завершили доступ к ресурсу, метод вызывает EventWaitHandle.Set метод, SyncUpdateResource который сигнализирует основному потоку продолжить выполнение.

Каждый вызов метода UnSyncUpdateResource вызывает внутренний метод UnSyncResource.Access, а затем вызывает метод Interlocked.Decrement для уменьшения счетчика numOps. Еще раз метод используется для уменьшения счетчика, чтобы убедиться, Interlocked.Decrement что второй поток не обращается к значению, прежде чем для переменной назначено отложенное значение первого потока. Когда последний несинхронизированный рабочий поток уменьшает счетчик до нуля, указывая, что больше несинхронизированных потоков не требуется для доступа к ресурсу, метод вызывает метод, UnSyncUpdateResource который сигнализирует EventWaitHandle.Set основному потоку продолжить выполнение.

Как показывает результат этого примера, синхронизированный доступ обеспечивает, что вызывающий поток выходит из защищенного ресурса до того, как другой поток получит доступ к этому ресурсу; каждый поток ожидает своего предшественника. С другой стороны, без блокировки метод UnSyncResource.Access вызывается в том порядке, в котором потоки получают к нему доступ.