Приветствую всех, сегодня поговорим о продолжении темы связанной с потоками, это часть 3. Читать Потоки ч.1 Читать Потоки ч.2
Эта часть посвящается синхронизации потоков при помощи объектов ядра системы Windows.
Thread Pool Пул потоков — это коллекция потоков, которые могут использоваться для выполнения нескольких задач в фоновом режиме. Пул потоков позволяет разгрузить главный поток для асинхронного выполнения других задач.
ThreadPool — класс предоставляющий пул потоков, который может использоваться для выполнения задач, отправки рабочих элементов, обработки асинхронного ввода-вывода, ожидания от имени других потоков и обработки таймеров.
Класс WaitHandle обычно используется в качестве базового для объектов синхронизации. Классы, производные от WaitHandle, определяют механизм сигнализации о предоставлении или освобождении монопольного доступа к общему ресурсу, но используют унаследованные методы WaitHandle для блокирования во время ожидания доступа к общим ресурсам.
Класс EventWaitHandle позволяет потокам взаимодействовать друг с другом путем передачи сигналов. Обычно один или несколько потоков блокируются на EventWaitHandle до тех пор пока не заблокированные потоки не вызывают метод Set для освобождения одного или нескольких заблокированных потоков.
Класс AutoResetEvent позволяет потокам взаимодействовать друг с другом путем передачи сигналов. Как правило, этот класс используется, когда потокам требуется исключительный доступ к ресурсу. Поток ожидает сигнала, вызывая метод WaitOne при возникновении AutoResetEvent. Если AutoResetEvent находится в несигнальном состоянии, поток будет заблокирован, ожидая сигнала потока, в настоящий момент контролирующего ресурс, о том, что ресурс доступен (для этого вызывается метод Set). Вызов Set сигнализирует событию AutoResetEvent о необходимости освобождения ожидающего потока. Событие AutoResetEvent остается в сигнальном состоянии до освобождения одного ожидающего потока, а затем
возвращается в не сигнальное состояние. Если нет ожидающих потоков, состояние остается сигнальным бесконечно. Если поток вызывает метод WaitOne, а AutoResetEvent находится в сигнальном состоянии, поток не блокируется. AutoResetEvent немедленно освобождает поток и возвращается в не сигнальное состояние.
Класс ManualResetEvent позволяет потокам взаимодействовать друг с другом путем передачи сигналов. Обычно это взаимодействие касается задачи, которую один поток должен завершить до того, как другой продолжит работу. Когда поток начинает работу, которая должна быть завершена до продолжения работы других потоков, он вызывает метод Reset для того, чтобы поместить ManualResetEvent в не сигнальное состояние. Этот поток можно понимать как контролирующий ManualResetEvent. Потоки, которые вызывают метод WaitOne в ManualResetEvent, будут заблокированы, ожидая сигнала.Когда контролирующий поток завершит работу, он вызовет метод Set для сообщения о том, что ожидающие потоки могут продолжить работу. Все ожидающие потоки освобождаются. ManualResetEvent остается в сигнальном состоянии до того момента, как оно будет снова установлено вручную. То есть, вызовы к WaitOne немедленно возвращаются. Можно контролировать начальное состояние ManualResetEvent, передав конструктору логическое значение, значение true, если начальное состояние сигнальное, и false, в противном случае.
Класс Mutex. Когда двум или более потокам одновременно требуется доступ к общему ресурсу, системе необходим механизм синхронизации, чтобы обеспечить использование ресурса только одним потоком одновременно. Mutex — примитив, который предоставляет эксклюзивный доступ к общему ресурсу только одному потоку. Если поток получает семафор, второй поток, желающий получить этот семафор, приостанавливается до тех пор, пока первый поток не освободит семафор.
Класс Semaphore используется для управления доступом к пулу ресурсов. Потоки производят вход в семафор, вызывая метод WaitOne, унаследованный от класса WaitHandle, и освобождают семафор вызовом метода Release.
Класс SemaphoreSlim — упрощенная альтернатива семафору Semaphore, ограничивающая количество потоков, которые могут параллельно обращаться к ресурсу или пулу ресурсов.
SemaphoreSlim предоставляет облегченный класс семафора, который не использует семафоры ядра Windows.
Класс Timer — предоставляет механизм для выполнения метода в заданные интервалы времени.
Сейчас я покажу пример, который покажется с одной стороны простым, но сложным с точки зрения его работы.
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 |
class Program { static int block =0; public static void Task1() { int i = 0; while (block==0) { i++; //Thread.Sleep(200); //Console.Write("*"); } Console.WriteLine("Вышли за пределы цикла"); //вот сюда мы никогда не попадем } static void Main(string[] args) { Thread task1 = new Thread(Task1); Console.WriteLine("Запустили поток и ожидаем 2 сек."); task1.Start(); Thread.Sleep(2000); block = 1; Console.WriteLine("Попытка завершения потока"); task1.Join(); Console.ReadKey(); } } |
ВНИМАНИЕ! Оптимизация не производиться в режиме отладки (DEBUG).
Если его запустить в режиме Release то мы никогда не покинем приделы цикла, и будем находится в мертвой петле бесконечно. Однако если раскоментим
1 2 |
//Thread.Sleep(200); //Console.Write("*"); |
Эти строки, то мы сможем выйти из цикла. Почему так спросите вы, а все дело в том что наш код проводит оптимизацию, компилятор видит, что переменная block присвоено 0 и она не изменяется в потоке. Он как бы делает одну проверку while, и дальше просто в него не заходит. И сколько бы мы не пытались изменить переменную, он этого не увидит, так как не осуществляет проверку условий.
Все это можно исправить несколькими способами, сейчас мы их и рассмотрим:
Ключевое слово volatile можно применять к полям следующих типов:
1. Ссылочные типы.
2. Типы: sbyte, byte, short, ushort, int, uint, char, float и bool.
3. Тип перечисления с одним из следующих базовых типов: byte, sbyte, short, ushort, int или uint.
4. Параметры универсальных типов, являющиеся ссылочными типами.
- Ключевое слово volatile можно применить только к полям класса или структуры.
- Локальные переменные не могут быть объявлены как volatile.
Поля, объявленные как volatile, не проходят оптимизацию компилятором, которая предусматривает доступ посредством отдельного потока. Это гарантирует наличие наиболее актуального значения в поле в любое время. Ключевое слово гарантирует что при чтении и записи манипуляция будет происходить непосредственно с памятью, а не со значениями, которые кэшированы в регистры процессора.
В нашем примере, достаточно добавить ключевое слово и получится:
1 |
static volatile int block =0; |
Или же мы можем использовать статические методы Thread.VolatileWrite и Thread.VolatileRead
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 |
class Program { static volatile int block =0; public static void Task1() { int i = 0; while (Thread.VolatileRead(ref block)==0) { i++; } Console.WriteLine("Вышли за пределы цикла"); } static void Main(string[] args) { Thread task1 = new Thread(Task1); Console.WriteLine("Запустили поток и ожидаем 2 сек."); task1.Start(); Thread.Sleep(2000); Thread.VolatileWrite(ref block, 1); Console.WriteLine("Попытка завершения потока"); task1.Join(); Console.ReadKey(); } } |
Пример работы пулом потоков:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public static void Task1(Object state) { Thread.Sleep(2000); Console.WriteLine("Поток №1"); Console.WriteLine(state.ToString()); } public static void Task2(Object state) { Console.WriteLine("Поток №2"); } static void Main(string[] args) { object obj = new object(); obj = "nookery.ru"; ThreadPool.QueueUserWorkItem(new WaitCallback(Task1),obj); ThreadPool.QueueUserWorkItem(Task2); Console.ReadKey(); } |
В данном примере мы создали 2 потока, в первый из них передали строковый параметр и с задержкой в 2 секунды вывели номер потока и сам параметр. Так же продемонстрировал, сокращенную запись и полную вариацию создания пула потоков. Так же мы видим отсутствие такого метода как Start, и простоту работы.
Все мы помним lock который позволял нам синхронизировать поток, существует и альтернатива ему, и применения ее похоже на lock, это Mutex приведу пример его работы, позволяет синхронизировать потоки.
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 |
class Program { public static void Task1() { m.WaitOne(); Console.WriteLine("Зашел поток № "+Thread.CurrentThread.Name); Thread.Sleep(1000); Console.WriteLine("Покинул поток №"+ Thread.CurrentThread.Name); Console.WriteLine(""); m.ReleaseMutex(); } static Mutex m = new Mutex(false, "MyMutex1"); static void Main(string[] args) { Thread.Sleep(5000); for(int i=0; i<5; i++) { Thread t = new Thread(Task1); t.Name = i.ToString(); t.Start(); } Console.ReadKey(); } } |
Mutex может использоваться любым потоком в процессе, который содержит ссылку на объект Mutex. Каждый не именованный объект Mutex представляет отдельный локальный мьютекс. Именованные системные мьютексы доступны в пределах всей операционной системы и могут быть использованы для синхронизации действий процессов. Можно создать объект Mutex, представляющий именованный системный мьютекс, используя конструктор с поддержкой имен. Объект операционной системы может быть создан в то же время, или существовать до создания объекта Mutex. О чем это нам говорит? а все дело в том что у нас могут быть под программы, или запущенны несколько экземпляров программ, но синхронизация их будет единой в системе os, от одного мьютекса. Если взять наш пример за основу, и запустить нашу программу в 3 экземплярах одновременно, мы сможем наблюдать синхронизацию потоков:
Метод отрабатывается в каждом потоке друг за дружкой, поочередно в 3 экземплярах программы.
Ограничения Mutex в том что работа с одним ресурсом может быть в одном потоке, а вот использовать большее количество потоков с одним ресурсом нам поможет класс Semaphore
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 |
class Program { public static void Task1() { sem.WaitOne(); Console.WriteLine("Зашел поток № "+Thread.CurrentThread.Name); Thread.Sleep(1000); Console.WriteLine("Покинул поток №"+ Thread.CurrentThread.Name); Console.WriteLine(""); sem.Release(); } static Semaphore sem= new Semaphore(2,4,"MySemaphore"); static void Main(string[] args) { Console.SetWindowSize(20, 20); Thread.Sleep(1000); //sem.Release(2); for(int i=0; i<5; i++) { Thread t = new Thread(Task1); t.Name = i.ToString(); t.Start(); } Console.ReadKey(); } } |
В примере выше, мы создали 5 потов, в конструктор класса передали значения Semaphore(2,4,»MySemaphore»); 2- это число потоков которое будет запускать метод Task1, 4-это число максимального количества потоков с работой метода Task1 и MySemaphore- имя.
В случаи нашего примера, у нас одновременно будет запущенно 2 потока, и работа с методом Task1 будет осуществлена из 2 потоков. Если вы были внимательны в коде есть закомментированная строка sem.Release(2); этот метод позволяет снять ограничения минимального числа потоков, и тем самым у нас метод Task1 будет использован в 4 потоков, а не из 2 как было прежде.
AutoResetEvent позволяет потокам взаимодействовать друг с другом путем передачи сигналов. Как правило, этот класс используется, когда потокам требуется исключительный доступ к ресурсу. Вызов Set сигнализирует событию AutoResetEvent о необходимости освобождения ожидающего потока. Событие AutoResetEvent остается в сигнальном состоянии до
освобождения одного ожидающего потока, а затем возвращается в не сигнальное состояние. Если нет ожидающих потоков, состояние остается сигнальным бесконечно. Если поток вызывает метод WaitOne, а AutoResetEvent находится в сигнальном состоянии, поток не блокируется. AutoResetEvent немедленно освобождает поток и возвращается в не сигнальное состояние.
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 |
class Program { public static void Task1() { Console.WriteLine("Зашел поток № "+Thread.CurrentThread.Name); auto.WaitOne(); Console.WriteLine("Покинул поток №"+ Thread.CurrentThread.Name); Console.WriteLine(""); } static AutoResetEvent auto = new AutoResetEvent(false); static void Main(string[] args) { for(int i=0; i<5; i++) { Thread t = new Thread(Task1); t.Name = i.ToString(); t.Start(); } Thread.Sleep(300); Console.WriteLine(); for (int i=0; i<5;i++) { Console.ReadKey(); auto.Set(); } Console.ReadKey(); } } |
В примере выше, мы запускаем работу с методом Task1 одновременно из 5 поток, и переводим их спящее состояния с помощью события AutoResetEvent и его метода WaitOne. Далее мы входим в цикл в основном потоке, и при нажатии клавиши, создаем сигнал, который позволяет выводить из сна метод, тем самым завершая его работу в текущем потоке.
ManualResetEvent позволяет потокам взаимодействовать друг с другом путем передачи сигналов. Обычно это взаимодействие касается задачи, которую один поток должен завершить до того, как другой продолжит работу. Когда поток начинает работу, которая должна быть завершена до продолжения работы других потоков, он вызывает метод Reset для того, чтобы поместить ManualResetEvent в не сигнальное состояние. Этот поток можно понимать как контролирующий ManualResetEvent. Потоки, которые вызывают метод WaitOne в ManualResetEvent, будут заблокированы, ожидая сигнала. Когда контролирующий поток завершит работу, он вызовет метод Set для сообщения о том, что ожидающие потоки могут продолжить работу. Все ожидающие потоки освобождаются. ManualResetEvent событие похоже на AutoResetEvent, за исключением того что он позволяет разбудить одним разом все потоки.
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 |
public static void Task1() { Console.WriteLine("Зашел поток № "+Thread.CurrentThread.Name); manual.WaitOne(); Console.WriteLine("Покинул поток №"+ Thread.CurrentThread.Name); Console.WriteLine(""); } static ManualResetEvent manual = new ManualResetEvent(false); static void Main(string[] args) { for(int i=0; i<5; i++) { Thread t = new Thread(Task1); t.Name = i.ToString(); t.Start(); } Thread.Sleep(300); Console.WriteLine(); Console.ReadKey(); manual.Set(); Console.ReadKey(); } } //Зашел поток № 0 //Зашел поток № 1 //Зашел поток № 2 //Зашел поток № 3 //Зашел поток № 4 //Покинул поток №4 //Покинул поток №3 //Покинул поток №1 //Покинул поток №0 //Покинул поток №2 |
Все это конечно хорошо, остановка итд, однако это не позволительная роскошь, в растрате ресурсов системы. Представьте если таких остановок будет 100 или 1000, и эти все потоки будут висеть в ожидании возобновления своей работы, а система просто рухнет и не сможет возобновить потоки. И на помощь приходить метод RegisterWaitForSingleObject класса ThreadPool
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public static void Task1(object state, bool timedOut) { Console.ForegroundColor = ConsoleColor.Blue; Console.WriteLine("nookery.ru"); } static void Main(string[] args) { AutoResetEvent auto = new AutoResetEvent(false); ThreadPool.RegisterWaitForSingleObject(auto,Task1, null, 1000, false); Console.ReadKey(); } |
Как видим, мы создали бесконечный цикл с интервалом времени в 1 секунду, вызывая метод Task1, так же мы можем остановить это цикл посмотрев следующий пример.
1 2 3 4 5 6 7 8 |
static void Main(string[] args) { AutoResetEvent auto = new AutoResetEvent(false); RegisteredWaitHandle r= ThreadPool.RegisterWaitForSingleObject(auto,Task1, null, 1000, false); Thread.Sleep(5000); r.Unregister(auto); Console.ReadKey(); } |
Имеется возможность передать аргументы в метод, помещенный в цикл.
И хочу показать еще один аналог цикл интервала основанный на классе Timer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public static void Task1(Object stateInfo) { while (true) { Thread.Sleep(300); Console.ForegroundColor = ConsoleColor.Blue; Console.WriteLine("nookery.ru"); } } static void Main(string[] args) { AutoResetEvent auto = new AutoResetEvent(false); new Timer(Task1,auto,1000,250); Console.ReadKey(); } |
Как видим, в конструктор помещаются практически те же самы переменные как и в случаи с RegisterWaitForSingleObject
Рассмотрим еще одно событие EventWaitHandle которое одновременно позволяет снять ожидание, со всех зарегистрированных программ.
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 |
public static void Task1() { manual.WaitOne(); while (true) { Thread.Sleep(300); Console.ForegroundColor = ConsoleColor.Blue; Console.WriteLine("nookery.ru"); } } static EventWaitHandle manual = null; static void Main(string[] args) { manual = new EventWaitHandle(false, EventResetMode.ManualReset, "MyEvent::1234"); Thread thread = new Thread(Task1); thread.IsBackground = true; thread.Start(); Console.WriteLine("Нажмите любую клавишу для начала работы потока."); Console.ReadKey(); manual.Set(); Console.ReadKey(); } |
Если запустить несколько экземпляров этой программы и нажать клавишу, мы увидим работу метода Task1 во всех запущенных копиях.
Если объект ядра с именем MyEvent уже существует будет получена ссылка на него. false — несигнальное состояние. ManualReset — тип событиия. MyEvent — имя по которому все приложения будут слушать событие,а 1234 это id который позволит индифицировать события в схожих приложениях.
На этом подошли к концу 3 части о потоках.
Читать Потоки 4 ч.