Средства синхронизации
Средства синхронизации
Синхронизация необходима для координации выполнения потоков. Такая координация необходима для согласования порядка выполнения потоков или для согласования доступа потоков к разделяемому ресурсу.
Среда Framework .NET предоставляет широкий набор средств синхронизации.
Блокировка | Join, Sleep, SpinWait |
Взаимно-исключительный доступ | Lock, Monitor, Mutex, SpinLock |
Сигнальные сообщения | AutoResetEvent, ManualResetEvent, ManualResetEventSlim |
Семафоры | Semaphore, SemaphoreSlim |
>Атомарные операторы | Interlocked |
Конкурентные коллекции | ConcurrentBag, ConcurrentQueue,ConcurrentDictionary, ConcurrentStack,BlockedCollection |
Блокировки чтения-записи | ReaderWriterLock, ReaderWriterLockSlim |
Шаблоны синхронизации | Barrier, CountdownEvent |
В основе синхронизации лежит понятие блокировки – один поток блокируется в ожидании определенного события от других потоков, например, завершения работы определенного потока или освобождения разделяемого ресурса.
Ожидание может быть активным или пассивным. При активном "ожидании" поток циклически проверяет статус ожидаемого события.
Thread thr = new Thread(SomeWork); thr.Start(); while(thr.IsAlive) ;
Такая блокировка называется активной, так как фактически поток не прекращает своей работы и не освобождает процессорное время для других потоков. Активное ожидание эффективно только при незначительном времени ожидания.
Пассивное ожидание реализуется с помощью операционной системы, которая сохраняет контекст потока и выгружает его, предоставляя возможность выполняться другим потокам. При наступлении ожидаемого события операционная система "будит" поток – загружает контекст потока и выделяет ему процессорное время. Пассивное ожидание требует времени на сохранение контекста потока при блокировке и загрузку контекста при возобновлении работы потока, но позволяет использовать вычислительные ресурсы во время ожидания для выполнения других задач.
В следующем фрагменте используются два типа ожидания. В первом случае применяется циклическая проверка статуса. Во втором случае используется метод Join.
class Program { static bool b; static double res; static void SomeWork() { for (int i=0; i<100000; i++) for(int j=0; j<20; j++) res += Math.Pow(i, 1.33); b = true; } static void Main() { Thread thr1 = new Thread(SomeWork); thr1.Start(); // Активное ожидание в цикле while(!b) ; Console.WriteLine("Result = " + res); res = 0; Thread thr2 = new Thread(SomeWork); thr2.Start(); // Ожидание с выгружением контекста thr2.Join(); } }
Анализ выполнения программы с помощью инструмента Visual Studio 12 "Визуализатор параллелизма" позволяет зафиксировать особенности загрузки вычислительной системы.
Загрузка процессора при активном ожидании в среднем равна 92%
При пассивном ожидании, основной поток выгружается и занятость ЦП в среднем равна 50%.
Существуют гибридные средства синхронизации, сочетающие в себе достоинства активного и пассивного ожидания. Гибридную блокировку используют объекты синхронизации, введенные в .NET 4.0: SpinWait, SpinLock, SemaphoreSlim, ManualResetEventSlim, ReaderWriteLockSlim и др. Потоки, блокируемые с помощью гибридных средств синхронизации, в начале фазы ожидания находятся в активном состоянии – не выгружаются, циклически проверяют статус ожидаемого события. Если ожидание затягивается, то активная блокировка становится не эффективной и операционная система выгружает ожидающий поток.
Средства для взаимного исключения
Одно из основных назначений средств синхронизации заключается в организации взаимно исключительного доступа к разделяемому ресурсу. Изменения общих данных одним потоком не должны прерываться другими потоками. Фрагмент кода, в котором осуществляется работа с разделяемым ресурсом и который должен выполняться только в одном потоке одновременно, называется критической секцией.
public string data; void DoSomeWork1() { Thread.Name = "First"; data = "AAAA"; Console.WriteLine("Thread: {0}, Data: {1}", Thread.Name, data); } void DoSomeWork2() { Thread.Name = "Second"; data = "BBBB"; Console.WriteLine("Thread: {0}, Data: {1}", Thread.Name, data); }
В этом фрагменте предполагается, что два потока изменяют значение общей переменной data. После внесения изменений поток выводит на экран новое значение data и имя потока. Отсутствие средств синхронизации могло бы привести к некорректному результату:
Thread: First, Data: BBBBB Thread: Second, Data: BBBBB
Поток First внес свои изменения и собирался вывести сообщение, но второй поток успел вклиниться и изменить данные.
Выделение критической секции с помощью конструкции lock позволяет избежать такой ситуации:
lock(sync_obj) { data = "AAAA"; Console.WriteLine("Thread #1 has changed data to: {0}", data); }
Когда один поток входит в критическую секцию (захватывает объект синхронизации), другой поток ожидает завершения всего блока (освобождения объекта синхронизации). Если заблокировано было несколько потоков, то при освобождении критической секции только один поток разблокируется и входит критическую секцию.
Для выделения критической секции с помощью конструкции lock необходимо указать объект синхронизации, который выступает в качестве идентификатора блокировки.