Синхронизация потоков (C# и Visual Basic)
В следующих разделах описаны функции и классы, которые можно использовать для синхронизации доступа к ресурсам в многопоточных приложениях.
Одним из преимуществ использования нескольких потоков в приложении является асинхронное выполнение каждого потока. В приложениях Windows это позволяет выполнять длительные задачи в фоновом режиме, при этом окно приложения и элементы управления остаются активными. Для серверных приложений многопоточность обеспечивает возможность обработки каждого входящего запроса в отдельном потоке. В противном случае ни один новый запрос не будет обработан, пока не завершена обработка предыдущего запроса.
Однако вследствие того, что потоки асинхронные, доступ к ресурсам, таким как дескрипторы файлов, сетевые подключения и память, должен быть скоординирован. Иначе два или более потоков могут получить доступ к одному и тому же ресурсу одновременно, причем один поток не будет учитывать действия другого. В результате данные могут быть повреждены непредсказуемым образом.
Для простых операций над числовыми типами данных синхронизация потоков выполняется с помощью членов класса Interlocked. Для прочих типов данных и других ресурсов, не являющихся потокобезопасными, многопоточность можно применять только с помощью структур, описываемых в этом разделе.
Дополнительные сведения о многопоточном программировании см. в разделах:
Ключевые слова Lock и SyncLock
Выражения lock (C#) и SyncLock (Visual Basic) используются для того, чтобы выполнение блока кода не прерывалось кодом, выполняемым в других потоках. Для этого нужно получить взаимоисключающую блокировку для данного объекта на время длительности блока кода.
Оператор lock или SyncLock получает объект в качестве аргумента, и за ним следует блок кода, который должен выполняться одновременно только в одном потоке. Примеры.
Public Class TestThreading
Dim lockThis As New Object
Public Sub Process()
SyncLock lockThis
' Access thread-sensitive resources.
End SyncLock
End Sub
End Class
public class TestThreading
{
private System.Object lockThis = new System.Object();
public void Process()
{
lock (lockThis)
{
// Access thread-sensitive resources.
}
}
}
Аргумент, предоставляемый ключевому слову lock, должен быть объектом на основе ссылочного типа; он используется для определения области блокировки. В приведенном выше примере область блокировки ограничена этой функцией, поскольку не существует ссылок на объект lockThis вне функции. Если бы такая ссылка существовала, область блокировки включала бы этот объект. Строго говоря, предоставляемый объект используется только для того, чтобы уникальным образом определить ресурс, к которому предоставляется доступ для различных потоков, поэтому это может быть произвольный экземпляр класса. В действительности этот объект обычно представляет ресурс, для которого требуется синхронизация потоков. Например, если объект контейнера должен использоваться в нескольких потоках, то контейнер можно передать блокировке, а блок синхронизированного кода после блокировки должен получить доступ к контейнеру. Если другие потоки блокируются для того же контейнера перед доступом к нему, обеспечивается безопасная синхронизация доступа к объекту.
Как правило, рекомендуется избегать блокировки типа public или экземпляров объектов, которыми не управляет код вашего приложения. Например, использование lock(this) может привести к неполадкам, если к экземпляру разрешен открытый доступ, поскольку внешний код также может блокировать объект. Это может привести к созданию ситуаций взаимной блокировки, когда два или несколько потоков будут ожидать высвобождения одного и того же объекта. По этой же причине блокировка открытого типа данных (в отличие от объектов) может привести к неполадкам. Блокировка строковых литералов наиболее опасна, поскольку строковые литералы интернируются средой CLR. Это означает, что если во всей программе есть один экземпляр любого строкового литерала, точно такой же объект будет представлять литерал во всех запущенных доменах приложения и во всех потоках. В результате блокировка, включенная для строки с одинаковым содержимым во всем приложении, блокирует все экземпляры этой строки в приложении. По этой причине лучше использовать блокировку закрытых или защищенных членов, для которых интернирование не применяется. В некоторых классах есть члены, специально предназначенные для блокировки. Например, в типе Array есть SyncRoot. Во многих типах коллекций есть член SyncRoot.
Для получения дополнительных сведений об операторах lock и SyncLock см. следующие разделы:
Мониторы
Как и ключевые слова lock и SyncLock, мониторы не допускают одновременное выполнение несколькими потоками одних и тех не блоков кода. Метод Enter позволяет только одному методу переходить к последующим операторам, все прочие методы заблокированы, пока выполняемый метод не вызовет Exit. Это аналогично использованию ключевого слова lock. Примеры.
SyncLock x
DoSomething()
End SyncLock
lock (x)
{
DoSomething();
}
Это соответствует следующей записи:
Dim obj As Object = CType(x, Object)
System.Threading.Monitor.Enter(obj)
Try
DoSomething()
Finally
System.Threading.Monitor.Exit(obj)
End Try
System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
DoSomething();
}
finally
{
System.Threading.Monitor.Exit(obj);
}
Использование ключевого слова lock (C#) или SyncLock (Visual Basic) предпочтительнее прямого использования класса Monitor, так как ключевые слова lock и SyncLock более компактны, а также так как слова lock и SyncLock обеспечивают разблокировку основного монитора даже в том случае, если защищенный код вызывает исключение. Для этого применяется ключевое слово finally, которые выполняет свой блок кода вне зависимости от наличия исключений.
События синхронизации и дескрипторы ожидания
Использование блокировки или монитора полезно для предотвращения одновременного выполнения блоков кода, но эти структуры не позволяют одному потоку передавать события в другой. Для этого требуются события синхронизации — объекты, обладающие одним их двух состояний (с сигналом или без сигнала), применяющиеся для активации и приостановки потоков. Потоки можно приостанавливать, заставляя их ожидать события синхронизации без сигнала, и активировать, меняя состояние события на состояние с сигналом. Если поток попытается ожидать события, для которого уже есть сигнал, то выполнение потока продолжится без задержки.
Существует два типа событий синхронизации: AutoResetEvent и ManualResetEvent. Отличие только одно: AutoResetEvent автоматически изменяется с состояния с сигналом на состояние без сигнала всегда при активации потока. В отличие от него, ManualResetEvent позволяет активировать состоянием с сигналом любое количество потоков, и вернется в состояние без сигнала только при вызове своего метода Reset.
Потоки можно заставить дожидаться определенных событий, вызвав один из методов ожидания, например WaitOne, WaitAny или WaitAll. WaitHandle.WaitOne() приводит к ожиданию потока до тех пор, пока единственное событие не становится сигнализирующим, WaitHandle.WaitAny() блокирует поток до тех пор, пока одно или несколько указанных событий не становятся сигнализирующими, и WaitHandle.WaitAll() блокирует поток до тех пор, пока все указанные события не становятся сигнализирующими. Событие выдает сигнал при вызове метода Set этого события.
В следующем примере поток создается и запускается функцией Main. Новый поток ждет события с помощью метода WaitOne. Поток приостанавливается до получения сигнала от события основным потоком, выполняющим функцию Main. После получения сигнала возвращается дополнительный поток. В этом случае, поскольку событие используется только для активации одного потока, можно использовать классы AutoResetEvent или ManualResetEvent.
Imports System.Threading
Module Module1
Dim autoEvent As AutoResetEvent
Sub DoWork()
Console.WriteLine(" worker thread started, now waiting on event...")
autoEvent.WaitOne()
Console.WriteLine(" worker thread reactivated, now exiting...")
End Sub
Sub Main()
autoEvent = New AutoResetEvent(False)
Console.WriteLine("main thread starting worker thread...")
Dim t As New Thread(AddressOf DoWork)
t.Start()
Console.WriteLine("main thread sleeping for 1 second...")
Thread.Sleep(1000)
Console.WriteLine("main thread signaling worker thread...")
autoEvent.Set()
End Sub
End Module
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.
Класс Interlocked
Методы класса Interlocked можно использовать для предотвращения проблем, возникающих при одновременной попытке нескольких потоков обновить или сравнить некоторое значение. Методы этого класса позволяют безопасно увеличивать, уменьшать, заменять и сравнивать значения переменных из любого потока.
Блокировки чтения и записи
В некоторых случаях может понадобиться блокировать ресурс только для записи данных и разрешить нескольким клиентам одновременно считывать данные, когда они не обновляются. Класс ReaderWriterLock обеспечивает монопольный доступ к ресурсу на то время, в течение которого поток изменяет ресурс, но разрешает одновременно выполнять несколько операций чтения. Блокировки чтения и записи являются удобной альтернативой монопольным блокировкам, которые заставляют другие потоки находиться в состоянии ожидания, даже когда им не нужно обновлять данные.
Взаимоблокировки
В многопоточных приложениях не обойтись без синхронизации потоков, однако всегда существует опасность создания deadlock, когда несколько потоков ожидают друг друга, и приложение зависает. Взаимоблокировка аналогична ситуации, в которой автомобили останавливаются на перекрестке и каждый водитель ожидает, пока проедет другой. Важно исключить возможность взаимоблокировок путем тщательного планирования. Взаимоблокировки часто можно предвидеть еще до написания кода, построив диаграмму многопотокового приложения.
Связанные разделы
Практическое руководство. Использование пула потоков (C# и Visual Basic)
Практическое руководство. Создание потока с использованием Visual C# .NET
Практическое руководство. Отправка рабочего элемента в пул потоков с помощью Visual C# .NET
См. также
Ссылки
Оператор lock (Справочник по C#)
Основные понятия
Многопоточные приложения (C# и Visual Basic)
Синхронизация данных для многопоточности