Синхронизация потоков (Руководство по программированию на C#)

Обновлен: Ноябрь 2007

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

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

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

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

Дополнительные сведения о многопоточном программировании см. в разделах:

Ключевое слово lock

Ключевое слово lock используется для того, чтобы выполнение блока кода не прерывалось кодом, выполняемым в других потоках. Для этого нужно получить взаимоисключающую блокировку для данного объекта на время длительности блока кода.

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

public class TestThreading
{
    private System.Object lockThis = new System.Object();

    public void Function()
    {

        lock (lockThis)
        {
            // Access thread-sensitive resources.
        }
    }

}

Аргумент, предоставляемый ключевому слову lock, должен быть объектом на основе ссылочного типа; он используется для определения области блокировки. В приведенном выше примере область блокировки ограничена этой функцией, поскольку не существует ссылок на объект lockThis вне функции. Если бы такая ссылка существовала, область блокировки включала бы этот объект. Строго говоря, объект, предоставляемый для lock, используется только для того, чтобы уникальным образом определить ресурс, к которому предоставляется доступ для различных потоков, поэтому это может быть произвольный экземпляр класса. В действительности этот объект обычно представляет ресурс, для которого требуется синхронизация потоков. Например, если объект контейнера должен использоваться в нескольких потоках, то контейнер можно передать блокировке, а блок синхронизированного кода после блокировки должен получить доступ к контейнеру. Если другие потоки блокируются для того же контейнера перед доступом к нему, обеспечивается безопасная синхронизация доступа к объекту.

Как правило, рекомендуется избегать блокировки типа public или экземпляров объектов, которыми не управляет код вашего приложения. Например, использование lock(this) может привести к неполадкам, если к экземпляру разрешен открытый доступ, поскольку внешний код также может блокировать объект. Это может привести к созданию ситуаций взаимной блокировки, когда два или несколько потоков будут ожидать высвобождения одного и того же объекта. По этой же причине блокировка открытого типа данных (в отличие от объектов) может привести к неполадкам. Блокировка строковых литералов наиболее опасна, поскольку строковые литералы интернируются средой CLR. Это означает, что если во всей программе есть один экземпляр любого строкового литерала, точно такой же объект будет представлять литерал во всех запущенных доменах приложения и во всех потоках. В результате блокировка, включенная для строки с одинаковым содержимым во всем приложении, блокирует все экземпляры этой строки в приложении. По этой причине лучше использовать блокировку закрытых или защищенных членов, для которых интернирование не применяется. В некоторых классах есть члены, специально предназначенные для блокировки. Например, в типе Array есть SyncRoot. Во многих типах коллекций есть член SyncRoot.

Дополнительные сведения о ключевом слове lock см. в разделе:

Мониторы

Как и lock, мониторы не допускают одновременное выполнение несколькими потоками одних и тех не блоков кода. Метод Enter позволяет только одному методу переходить к последующим операторам, все прочие методы заблокированы, пока выполняемый метод не вызовет Exit. Это аналогично использованию ключевого слова lock. Ключевое слово lock реализовано с классом Monitor. Пример.

lock (x)
{
    DoSomething();
}

Соответствует следующему:

System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
    DoSomething();
}
finally
{
    System.Threading.Monitor.Exit(obj);
}

Рекомендуется использовать ключевое слово lock, а не класс Monitor, поскольку lock компактнее и потому что lock обеспечивает высвобождение монитора даже если защищенный код создал исключение. Для этого применяется ключевое слово finally, которые выполняет свой блок кода вне зависимости от наличия исключений.

Дополнительные сведения о мониторах см. в разделе Пример Monitor Synchronization Technology.

События синхронизации и дескрипторы ожидания

Использование блокировки или монитора полезно для предотвращения одновременного выполнения блоков кода, но эти структуры не позволяют одному потоку передавать события в другой. Для этого требуются события синхронизации — объекты, обладающие одним их двух состояний (с сигналом или без сигнала), применяющиеся для активации и приостановки потоков. Потоки можно приостанавливать, заставляя их ожидать события синхронизации без сигнала, и активировать, меняя состояние события на состояние с сигналом. Если поток попытается ожидать события, для которого уже есть сигнал, то выполнение потока продолжится без задержки.

Существует два типа событий синхронизации: AutoResetEvent и ManualResetEvent. Отличие только одно: AutoResetEvent автоматически изменяется с состояния с сигналом на состояние без сигнала всегда при активации потока. В отличие от него, ManualResetEvent позволяет активировать состоянием с сигналом любое количество потоков, и вернется в состояние без сигнала только при вызове своего метода Reset.

Можно заставить потоки ожидать событий путем вызова одного из методов ожидания, например WaitOne, WaitAny или WaitAll. Метод WaitHandle.WaitOne() заставляет поток ждать сигнала одиночного события, метод WaitHandle.WaitAny() заставляет поток ждать сигнала одного или нескольких указанных событий, а метод WaitHandle.WaitAll() блокирует поток до получения сигнала от всех указанных событий. Событие выдает сигнал при вызове метода Set этого события.

В следующем примере поток создается и запускается функцией Main. Новый поток ждет события с помощью метода WaitOne. Поток приостанавливается до получения сигнала от события основным потоком, выполняющим функцию Main. После получения сигнала возвращается дополнительный поток. В этом случае, поскольку событие используется только для активации одного потока, можно использовать классы AutoResetEvent или ManualResetEvent.

using System;
using System.Threading;

class ThreadingExample
{
    static AutoResetEvent autoEvent;

    static void DoWork()
    {
        Console.WriteLine("   worker thread started, now waiting on event...");
        autoEvent.WaitOne();
        Console.WriteLine("   worker thread reactivated, now exiting...");
    }

    static void Main()
    {
        autoEvent = new AutoResetEvent(false);

        Console.WriteLine("main thread starting worker thread...");
        Thread t = new Thread(DoWork);
        t.Start();

        Console.WriteLine("main thread sleeping for 1 second...");
        Thread.Sleep(1000);

        Console.WriteLine("main thread signaling worker thread...");
        autoEvent.Set();
    }
}

Дополнительные примеры использования событий синхронизации потоков см. в разделе:

Мьютексные объекты

Мьютекс аналогичен монитору, он не допускает одновременного выполнения блока кода более чем из одного потока. Название "мьютекс" – сокращенная форма слова "взаимоисключающий" ("mutually exclusive" на английском языке). Впрочем, в отличие от мониторов мьютексы можно использовать для синхронизации потоков по процессам. Мьютекс представляется классом Mutex.

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

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

Связанные разделы

См. также

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

Руководство по программированию в C#

Ссылки

Thread

WaitOne

WaitAny

WaitAll

Monitor

Mutex

AutoResetEvent

ManualResetEvent

Interlocked

WaitHandle

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

Реализация асинхронной модели программирования CLR

Упрощенное автоматическое управление питанием (APM) с использованием C#

Монитор взаимоблокировок