Выбрать главу
ыл известен где-нибудь еще. Для этого он должен быть именованным. Ниже приведены формы конструктора, предназначенные для создания такого мьютекса. public Mutex(bool initiallyOwned, string имя) public Mutex(bool initiallyOwned, string имя, out bool createdNew) В обеих формах конструктора имя обозначает конкретное имя мьютекса. Если в первой форме конструктора параметр initiallyOwned имеет логическое значение true, то владение мьютексом запрашивается. Но поскольку мьютекс может принад лежать другому процессу на системном уровне, то для этого параметра лучше указать логическое значение false. А после возврата из второй формы конструктора пара метр createdNew будет иметь логическое значение true, если владение мьютексом было запрошено и получено, и логическое значение false, если запрос на владение был отклонен. Существует и третья форма конструктора типа Mutex, в которой допу скается указывать управляющий доступом объект типа MutexSecurity. С помощью именованных мьютексов можно синхронизировать взаимодействие процессов. И последнее замечание: в потоке, получившем мьютекс, допускается делать один иди несколько дополнительных вызовов метода WaitOne() перед вызовом метода ReleaseMutex(), причем все эти дополнительные вызовы будут произведены успеш но. Это означает, что дополнительные вызовы метода WaitOne() не будут блокировать поток, который уже владеет мьютексом. Но количество вызовов метода WaitOne() должно быть равно количеству вызовов метода ReleaseMutex() перед освобождени ем мьютекса. Семафор Семафор подобен мьютексу, за исключением того, что он предоставляет одновре менный доступ к общему ресурсу не одному, а нескольким потокам. Поэтому семафор пригоден для синхронизации целого ряда ресурсов. Семафор управляет доступом к общему ресурсу, используя для этой цели счетчик. Если значение счетчика больше нуля, то доступ к ресурсу разрешен. А если это значение равно нулю, то доступ к ре сурсу запрещен. С помощью счетчика ведется подсчет количества разрешений. Следова тельно, для доступа к ресурсу поток должен получить разрешение от семафора. Обычно поток, которому требуется доступ к общему ресурсу, пытается получить разрешение от семафора. Если значение счетчика семафора больше нуля, то поток получает разрешение, а счетчик семафора декрементируется. В противном случае по ток блокируется до тех пор, пока не получит разрешение. Когда же потоку больше не требуется доступ к общему ресурсу, он высвобождает разрешение, а счетчик семафора инкрементируется. Если разрешения ожидает другой поток, то он получает его в этот момент. Количество одновременно разрешаемых доступов указывается при создании семафора. Так, если создать семафор, одновременно разрешающий только один до ступ, то такой семафор будет действовать как мьютекс. Семафоры особенно полезны в тех случаях, когда общий ресурс состоит из группы иди пуда ресурсов. Например, пул ресурсов может состоять из целого ряда сетевых соединений, каждое из которых служит для передачи данных. Поэтому потоку, кото рому требуется сетевое соединение, все равно, какое именно соединение он получит. В данном случае семафор обеспечивает удобный механизм управления доступом к се тевым соединениям. Семафор реализуется в классе System.Threading.Semaphore, у которого имеется несколько конструкторов. Ниже приведена простейшая форма конструктора данного класса: public Semaphore(int initialCount, int maximumCount) где initialCount — это первоначальное значение для счетчика разрешений сема фора, т.е. количество первоначально доступных разрешений; maximumCount — мак симальное значение данного счетчика, т.е. максимальное количество разрешений, ко торые может дать семафор. Семафор применяется таким же образом, как и описанный ранее мьютекс. В це лях получения доступа к ресурсу в коде программы вызывается метод WaitOne() для семафора. Этот метод наследуется классом Semaphore от класса WaitHandle. Метод WaitOne() ожидает до тех пор, пока не будет получен семафор, для которого он вы зывается. Таким образом, он блокирует выполнение вызывающего потока до тех пор, пока указанный семафор не предоставит разрешение на доступ к ресурсу. Если коду больше не требуется владеть семафором, он освобождает его, вызывая метод Release(). Ниже приведены две формы этого метода. public int Release() public int Release(int releaseCount) В первой форме метод Release() высвобождает только одно разрешение, а во второй форме — количество разрешений, определяемых параметром releaseCount. В обеих формах данный метод возвращает подсчитанное количество разрешений, су ществовавших до высвобождения. Метод WaitOne() допускается вызывать в потоке несколько раз перед вызовом ме тода Release(). Но количество вызовов метода WaitOne() должно быть равно ко личеству вызовов метода Release() перед высвобождением разрешения. С другой стороны, можно воспользоваться формой вызова метода Release(int num), чтобы передать количество высвобождаемых разрешений, равное количеству вызовов метода WaitOne(). Ниже приведен пример программы, в которой демонстрируется применение се мафора. В этой программе семафор используется в классе MyThread для одновремен ного выполнения только двух потоков типа MyThread. Следовательно, разделяемым ресурсом в данном случае является ЦП. // Использовать семафор. using System; using System.Threading; // Этот поток разрешает одновременное выполнение // только двух своих экземпляров. class MyThread { public Thread Thrd; // Здесь создается семафор, дающий только два // разрешения из двух первоначально имеющихся. static Semaphore sem = new Semaphore(2, 2); public MyThread(string name) { Thrd = new Thread(this.Run); Thrd.Name = name; Thrd.Start(); } // Точка входа в поток. void Run() { Console.WriteLine(Thrd.Name + " ожидает разрешения."); sem.WaitOne(); Console.WriteLine(Thrd.Name + " получает разрешение."); for(char ch='A'; ch < 'D'; ch++) { Console.WriteLine(Thrd.Name + " : " + ch + " "); Thread.Sleep(500); } Console.WriteLine(Thrd.Name + " высвобождает разрешение."); // Освободить семафор. sem.Release(); } } class SemaphoreDemo { static void Main() { // Сконструировать три потока. MyThread mt1 = new MyThread("Поток #1"); MyThread mt2 = new MyThread("Поток #2"); MyThread mt3 = new MyThread("Поток #3"); mt1.Thrd.Join(); mt2.Thrd.Join(); mt3.Thrd.Join(); } } В классе MyThread объявляется семафор sem, как показано ниже. static Semaphore sem = new Semaphore(2, 2); При этом создается семафор, способный дать не более двух разрешений на доступ к ресурсу из двух первоначально имеющихся разрешений. Обратите внимание на то, что выполнение метода MyThread.Run() не может быть продолжено до тех пор, пока семафор sem не даст соответствующее разрешение. Если разрешение отсутствует, то выполнение потока приостанавливается. Когда же разре шение появляется, выполнение потока возобновляется. В методе In Main() создаются три потока. Но выполняться могут только два первых потока, а третий должен ожи дать окончания одного из этих двух потоков. Ниже приведен результат выполнения рассматриваемой здесь программы, хотя у вас он может оказаться несколько иным. Поток #1 ожидает разрешения. Поток #1 получает разрешение. Поток #1 : А Поток #2 ожидает разрешения. Поток #2 получает разрешение. Поток #2 : А Поток #3 ожидает разрешения. Поток #1 : В Поток #2 : В Поток #1 : С Поток #2 : С Поток #1 высвобождает разрешение. Поток #3 получает разрешение. Поток #3 : А Поток #2 высвобождает разрешение. Поток #3 : В Поток #3 : С Поток #3 высвобождает разрешение. Семафор, созданный в предыдущем примере, известен только тому процессу, кото рый его породил. Но семафор можно создать и таким образом, чтобы он был известен где-нибудь еще. Для этого он должен быть именованным. Ниже приведены формы конструктора класса Semaphore, предназначенные для создания такого семафора. public Semaphore(int initialCount, int maximumCount, string имя) public Semaphore(int initialCount, int maximumCount, string имя, out bool createdNew) В обеих формах имя обозначает конкретное имя, передаваемое конструктору. Если в первой форме семафор, на который указывает имя, еще не существует, то он создает ся с помощью значений, определяемых параметрами initialCount и maximumCount. А если он уже существует, то значения параметров initialCount и maximumCount игнорируются. После возврата из второй формы конструктора параметр createdNew будет иметь логическое значение true, если семафор был создан. В этом случае значе ния параметров initialCount и maximumCount используются для создания семафора. Если же параметр createdNew будет иметь логическое значение false, значит, сема фор уже существует и значения параметров initialCount и maximumCount игнори руются. Существует и третья форма конструктора класса Semaphore, в которой допуска ется указывать управляющий доступом объект типа SemaphoreSecurity. С помощью именованных семафоров можно синхронизировать взаимодействие процессов. Применение событий Для синхронизации в C# предусмотрен еще один тип объекта: событие. Существу ют две разновидности событий: устанавливаемые в исходное состояние вручную и ав томатически. Они поддерживаются в классах ManualResetEvent и AutoResetEvent соответственно. Эти классы являются производными от класса EventWaitHandle, на ходящегося на верхнем уровне иерархии классов, и применяются в тех случаях, когда один поток ожидает появления некоторого события в другом потоке. Как только такое событие появляется, второй поток уведомляет о нем первый поток, позволяя тем са мым возобновить его выполнение. Ниже приведены конструкторы классов ManualResetEvent и AutoResetEvent. public ManualResetEvent(bool initialState) public AutoResetEvent(bool initialState) Если в обеих формах параметр initialState имеет логическое значение true, то о событии первоначально уведомляется. А если он имеет логическое значение false, то о событии первоначально не уведомляется. Применяются события очень просто. Так, для события типа ManualResetEvent порядок применения следующий. Поток, ожидающий некоторое событие, вызывает метод WaitOne() для событийного объекта, представляющего данное событие. Если событийный объект находится в сигнальном состоянии, то происходит немедленный возврат из метода WaitOne(). В противном случае выполнение вызывающего потока приостанавливается до тех пор, пока не будет получено уведомление о событии. Как только событие произойдет в другом потоке, этот поток установит событийный объект в сигнальное состояние, вызвав метод Set(). Поэтому метод Set() следует рассма тривать как уведомляющий о том, что событие произошло. После установки собы тийного объекта в сигнальное состояние произойдет немедленный возврат из метода WaitOne(), и первый поток возобновит свое выполнение. А в результате вызова мето да Reset() событийный объект возвращается в несигнальное состояние. Событие типа AutoResetEvent отличается от события типа ManualResetEvent лишь способом установки в исходное состояние. Если для события типа ManualResetEvent событийный объект остается в сигнальном состоянии до тех пор, пока не будет вызван метод Reset(), то для события типа AutoResetEvent собы тийный объект автоматически переходит в несигнальное состояние, как только поток, ожидающий это событие, получит уведомление о нем и возобновит свое выполне ние. Поэтому если применяется событие типа AutoResetEvent, то вызывать метод Reset() необязательно. В приведенном ниже примере программы демонстрируется применение события типа ManualResetEvent. // Использовать событийный объект, устанавливаемый // в исходное состояние вручную. using System; using System.Threading; // Этот поток уведомляет о том, что событие передано его конструктору. class MyThread { public Thread Thrd; ManualResetEvent mre; public MyThread(string name, ManualResetEvent evt) { Thrd = new Thread(this.Run); Thrd.Name = name; mre = evt; Thrd.Start(); } // Точка входа в поток. void Run() { Console.WriteLine("Внутри потока " + Thrd.Name); for(int i=0; i<5; i++) { Console.WriteLine(Thrd.Name); Thread.Sleep(500); } Console.WriteLine(Thrd.Name + " завершен!"); // Уведомить о событии. mre.Set(); } } class ManualEventDemo { static void Main() { ManualResetEvent evtObj = new ManualResetEvent(false); MyThread mt1 = new MyThread("Событийный Поток 1", evtObj); Console.WriteLine("Основной поток ожидает событие."); // Ожидать уведомления о событии. evtObj.WaitOne(); Console.WriteLine("Основной поток получил " + "уведомление о событии от первого потока."); // Установить событийный объект в исходное состояние. evtObj.Reset(); mt1 = new MyThread("Событийный Поток 2", evtObj); // Ожидать уведомления о событии. evtObj.WaitOne(); Console.WriteLine("Основной поток получил " + "уведомление о событии от второго потока."); } } Ниже приведен результат выполнения рассматриваемой здесь программы, хотя у вас он может оказаться несколько иным. В потоке Событийный Поток 1 Событийный Поток 1 Основной поток ожидает событие. Событийный Поток 1 Событийный Поток 1 Событийный Поток 1 Событийный Поток 1 Событийный Поток 1 завершен! Основной поток получил уведомление о событии от первого потока. В потоке Событийный Поток 2 Событийный Поток 2 Событийный Поток 2 Событийный Поток 2 Событийный Поток 2 Событийный Поток 2 Событийный Поток 2 завершен! Основной поток получил уведомление о событии от второго потока. Прежде всего обратите внимание на то, что событие типа ManualResetEvent передается непосредственно конструктору класса MyThread. Когда завершается ме тод Run() из класса MyThread, он вызывает для событийного объекта метод Set(), устанавливающий этот объект в сигнальное состояние. В методе Main() формирует ся событийный объект evtObj типа ManualResetEvent, первоначально устанавли ваемый в исходное, несигнальное состояние. Затем создается экземпляр объекта типа MyThread, которому передается событийный объект evtObj. После этого основной поток ожидает уведомления о событии. А поскольку событийный объект evtObj пер воначально находится в несигнальном состоянии, то основной поток вынужден ожи дать до тех пор, пока для экземпляра объекта типа MyThread не будет вызван метод Set(), устанавливающий событийный объект evtObj в сигнальное состояние. Это дает возможность основному потоку возобновить свое выполнение. Затем событийный объект устанавливается в исходное состояние, и весь процесс повторяется, но на этот раз для второго потока. Если бы не событийный объект, то все потоки выполнялись бы одновременно, а результаты их выполнения оказались бы окончательно запутан ными. Для того чтобы убедиться в этом, попробуйте закомментировать вызов метода WaitOne() в методе Main(). Если бы в рассматриваемой здесь программе событийный объект типа AutoResetEvent использовался вместо событийного объекта типа ManualResetEvent, то вызывать метод Reset() в методе Main() не пришлось бы. Ведь в этом случае со бытийный объект автоматически устанавливается в несигнальное состояние, когда по ток, ожидающий данное событие, возобновляет свое выполнение. Для опробования этой разновидности события замените в данной программе все ссылки на объект типа ManualResetEvent ссылками на объект типа AutoResetEvent и удалите все вызовы ме тода Reset(). Видоизмененная версия программы будет работать так же, как и прежде. Класс Interlocked Еще одним классом, связанным с синхронизацией, является класс Interlocked. Этот класс служит в качестве альтернативы другим средствам синхронизации, когда требуется только изменить значение общей переменной. Методы, доступные в классе Interlocked, гарантируют, что их действие будет выполняться как единая, непреры ваемая операция. Это означает, что никакой синхронизации в данном случае вообще не требуется. В классе Interlocked предоставляются статические методы для сложе ния двух целых значений, инкрементирования и декрементирования целого значения, сравнения и установки значений объекта, обмена объектами и получения 64-разрядно- го значения. Все эти операции выполняются без прерывания. В приведенном ниже примере программы демонстрируется применение двух ме тодов из класса Interlocked:Increment() и Decrement(). При этом используются следующие формы обоих методов: public static int Increment(ref int location) public static int Decrement(ref int location) где location — это переменная, которая подлежит инкрементированию или декре ментированию. // Использовать блокируемые операции. using System; using System.Threading; // Общий ресурс. class SharedRes { public static int Count = 0; } // В этом потоке переменная SharedRes.Count инкрементируется. class IncThread { public Thread Thrd; public IncThread(string name) { Thrd = new Thread(this.Run); Thrd.Name = name; Thrd.Start(); } // Точка входа в поток. void Run() { for(int i=0; i<5; i++) { Interlocked.Increment(ref SharedRes.Count); Console.WriteLine(Thrd.Name + " Count = " + SharedRes.Count); } } } // В этом потоке переменная SharedRes.Count декрементируется. class DecThread { public Thread Thrd; public DecThread(string name) { Thrd = new Thread(this.Run); Thrd.Name = name; Thrd.Start(); } // Точка входа в поток. void Run() { for(int i=0; i<5; i++) { Interlocked.Decrement(ref SharedRes.Count); Console.WriteLine(Thrd.Name + " Count = " + SharedRes.Count); } } } class InterlockedDemo { static void Main() { // Сконструировать два потока. IncThread mt1 = new IncThread("Инкрементирующий Поток"); DecThread mt2 = new DecThread("Декрементирующий Поток"); mt1.Thrd.Join(); mt2.Thrd.Join(); } } Классы синхронизации, внедренные в версии .NET Framework 4.0 Рассматривавшиеся ранее классы синхронизации, в том числе Semaphore и AutoResetEvent, были доступны в среде .NET Framework, начиная с версии 1.1. Таким образом, эти классы образуют основу поддержки синхронизации в среде .NET Framework. Но после выпуска версии .NET Framework 4.0 появился ряд новых альтер натив этим классам синхронизации. Все они перечисляются ниже. Класс Назначение Barrier Вынуждает потоки ожидать появления всех остальных пото ков в указанной точке, называемой барьерной CountdownEvent Выдает сигнал, когда обратный отсчет завершается ManualResetEventSlim Это упрощенный вариант класса ManualResetEvent SemaphoreSlim Это упрощенный вариант класса Semaphore Если вам понятно, как пользоваться основными классами синхронизации, описан ными ранее в этой главе, то у вас не должно возникнуть затруднений при использова нии их новых альтернатив и дополнений. Прерывание потока Иногда поток полезно прервать до его нормального завершения. Например, от ладчику может понадобиться прервать вышедший из-под контроля поток. После пре рывания поток удаляется из системы и не может быть начат снова. Для прерывания потока до его нормального завершения служит метод Thread. Abort(). Ниже приведена простейшая форма этого метода. public void Abort() Метод Abort() создает необходимые условия Для генерирования исключения ThreadAbortException в том потоке, для которого он был вызван. Это исключение приводит к прерыванию потока и может быть перехвачено и в коде программы, но в этом случае оно автоматически генерируется еще раз, чтобы остановить поток. Ме тод Abort() не всегда способен остановить поток немедленно, поэтому если поток требуется остановить перед тем, как продолжить выполнение программы, то после метода Abort() следует сразу же вызвать метод Join(). Кроме того, в самых редких случаях методу Abort() вообще не удается остановить поток. Это происходит, напри мер, в том случае, если кодовый блок finally входит в бесконечный цикл. В приведенном ниже примере программы демонстрируется применение метода Abort() для прерывания потока. // Прервать поток с помощью метода Abort(). using System; using System.Threading; class MyThread { public Thread Thrd; public MyThread(string name) { Thrd = new Thread(this.Run); Thrd.Name = name; Thrd.Start(); } // Это точка входа в поток. void Run() { Console.WriteLine(Thrd.Name + " начат."); for(int i = 1; i <= 1000; i++) { Console.Write(i + " "); if((i%10)==0) { Console.WriteLine(); Thread.Sleep(250); } } Console.WriteLine(Thrd.Name + " завершен."); } } class StopDemo { static void Main() { MyThread mt1 = new MyThread("Мой Поток"); Thread.Sleep(1000); // разрешить порожденному потоку начать свое выполнение Console.WriteLine("Прерывание потока."); mt1.Thrd.Abort(); mt1.Thrd.Join(); // ожидать прерывания потока Console.WriteLine("Основной поток прерван."); } } Вот к какому результату приводит выполнение этой программы. Мой Поток начат 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 Прерывание потока. Основной поток прерван. ПРИМЕЧАНИЕ Метод Abort() не следует применять в качестве обычного средства прерывания потока, поскольку он предназначен для особых случаев. Обычно поток должен завершаться естествен ным образом, чтобы произошел возврат из метода, выполняющего роль точки входа в него. Другая форма метода Abort() В некоторых случаях оказывается полезной другая форма метода Abort(), при веденная ниже в общем виде: public void Abort (object stateInfo) где stateInfo обозначает любую информацию, которую требуется передать по току, когда он останавливается. Эта информация доступна посредством свойства ExceptionState из класса исключения ThreadAbortException. Подобным образом потоку можно передать код завершения. В приведенном ниже примере программы демонстрирует