Выбрать главу
ть исходное значение суммы for(int i=0; i < nums.Length; i++) { sum += nums[i]; Console.WriteLine("Текущая сумма для потока " + Thread.CurrentThread.Name + " равна " + sum); Thread.Sleep(10); // разрешить переключение задач } return sum; } } } class MyThread { public Thread Thrd; int[] a; int answer; // Создать один объект типа SumArray для всех // экземпляров класса MyThread. static SumArray sa = new SumArray(); // Сконструировать новый поток, public MyThread(string name, int[] nums) { a = nums; Thrd = new Thread(this.Run); Thrd.Name = name; Thrd.Start(); // начать поток } // Начать выполнение нового потока. void Run() { Console.WriteLine(Thrd.Name + " начат."); answer = sa.SumIt(a); Console.WriteLine("Сумма для потока " + Thrd.Name + " равна " + answer); Console.WriteLine(Thrd.Name + " завершен."); } } class Sync { static void Main() { int[] a = {1, 2, 3, 4, 5); MyThread mt1 = new MyThread("Потомок #1", a); MyThread mt2 = new MyThread("Потомок #2", a); mt1.Thrd.Join(); mt2.Thrd.Join(); } } Ниже приведен результат выполнения данной программы, хотя у вас он может ока заться несколько иным. Потомок #1 начат. Текущая сумма для потока Потомок #1 равна 1 Потомок #2 начат. Текущая сумма для потока Потомок #1 равна 3 Текущая сумма для потока Потомок #1 равна 6 Текущая сумма для потока Потомок #1 равна 10 Текущая сумма для потока Потомок #1 равна 15 Текущая сумма для потока Потомок #2 равна 1 Сумма для потока Потомок #1 равна 15 Потомок #1 завершен. Текущая сумма для потока Потомок #2 равна 3 Текущая сумма для потока Потомок #2 равна 6 Текущая сумма для потока Потомок #2 равна 10 Текущая сумма для потока Потомок #2 равна 15 Сумма для потока Потомок #2 равна 15 Потомок #2 завершен. Как следует из приведенного выше результата, в обоих потоках правильно подсчи тывается сумма, равная 15. Рассмотрим эту программу более подробно. Сначала в ней создаются три класса. Первым из них оказывается класс SumArray, в котором определяется метод SumIt(), суммирующий элементы целочисленного массива. Вторым создается класс MyThread, в котором используется статический объект sa типа SumArray. Следовательно, един ственный объект типа SumArray используется всеми объектами типа MyThread. С по мощью этого объекта получается сумма элементов целочисленного массива. Обратите внимание на то, что текущая сумма запоминается в поле sum объекта типа SumArray. Поэтому если метод SumIt() используется параллельно в двух потоках, то оба потока попытаются обратиться к полю sum, чтобы сохранить в нем текущую сумму. А по скольку это может привести к ошибкам, то доступ к методу SumIt() должен быть синхронизирован. И наконец, в третьем классе, Sync, создаются два потока, в которых подсчитывается сумма элементов целочисленного массива. Оператор lock в методе SumIt() препятствует одновременному использованию данного метода в разных потоках. Обратите внимание на то, что в операторе lock объ ект lockOn используется в качестве синхронизируемого. Это закрытый объект, предна значенный исключительно для синхронизации. Метод Sleep() намеренно вызывается для того, чтобы произошло переключение задач, хотя в данном случае это невозмож но. Код в методе SumIt() заблокирован, и поэтому он может быть одновременно ис пользован только в одном потоке. Таким образом, когда начинает выполняться второй порожденный поток, он не сможет войти в метод SumIt() до тех пор, пока из него не выйдет первый порожденный поток. Благодаря этому гарантируется получение пра вильного результата. Для того чтобы полностью уяснить принцип действия блокировки, попробуйте удалить из рассматриваемой здесь программы тело метода SumIt(). В итоге метод SumIt() перестанет быть синхронизированным, а следовательно, он может парал лельно использоваться в любом числе потоков для одного и того же объекта. Посколь ку текущая сумма сохраняется в поле sum, она может быть изменена в каждом потоке, вызывающем метод SumIt(). Это означает, что если два потока одновременно вызы вают метод SumIt() для одного и того же объекта, то конечный результат получается неверным, поскольку содержимое поля sum отражает смешанный результат сумми рования в обоих потоках. В качестве примера ниже приведен результат выполнения рассматриваемой здесь программы после снятия блокировки с метода SumIt(). Потомок #1 начат. Текущая сумма для потока Потомок #1 равна 1 Потомок #2 начат. Текущая сумма для потока Потомок #2 равна 1 Текущая сумма для потока Потомок #1 равна 3 Текущая сумма для потока Потомок #2 равна 5 Текущая сумма для потока Потомок #1 равна 8 Текущая сумма для потока Потомок #2 равна 11 Текущая сумма для потока Потомок #1 равна 15 Текущая сумма для потока Потомок #2 равна 19 Текущая сумма для потока Потомок #1 равна 24 Текущая сумма для потока Потомок #2 равна 29 Сумма для потока Потомок #1 равна 29 Потомок #1 завершен. Текущая сумма для потока Потомок #2 равна 29 Потомок #2 завершен. Как следует из приведенного выше результата, в обоих порожденных потоках ме тод SumIt() используется одновременно для одного и того же объекта, а это приводит к искажению значения в поде sum. Ниже подведены краткие итоги использования блокировки. • Если блокировка любого заданного объекта получена в одном потоке, то после блокировки объекта она не может быть получена в другом потоке. • Остальным потокам, пытающимся получить блокировку того же самого объек та, придется ждать до тех пор, пока объект не окажется в разблокированном состоянии. • Когда поток выходит из заблокированного фрагмента кода, соответствующий объект разблокируется. Другой подход к синхронизации потоков Несмотря на всю простоту и эффективность блокировки кода метода, как показано в приведенном выше примере, такое средство синхронизации оказывается пригодным далеко не всегда. Допустим, что требуется синхронизировать доступ к методу класса, который был создан кем-то другим и сам не синхронизирован. Подобная ситуация вполне возможна при использовании чужого класса, исходный код которого недо ступен. В этом случае оператор lock нельзя ввести в соответствующий метод чужого класса. Как же тогда синхронизировать объект такого класса? К счастью, этот вопрос разрешается довольно просто: доступ к объекту может быть заблокирован из внеш него кода по отношению к данному объекту, для чего достаточно указать этот объ ект в операторе lock. В качестве примера ниже приведен другой вариант реализации предыдущей программы. Обратите внимание на то, что код в методе SumIt() уже не является заблокированным, а объект lockOn больше не объявляется. Вместо этого вы зовы метода SumIt() блокируются в классе MyThread. // Другой способ блокировки для синхронизации доступа к объекту. using System; using System.Threading; class SumArray { int sum; public int SumIt(int[] nums) { sum = 0; // установить исходное значение суммы for (int i=0; i < nums.Length; i++) { sum += nums[i]; Console.WriteLine("Текущая сумма для потока " + Thread.CurrentThread.Name + " равна " + sum); Thread.Sleep(10); // разрешить переключение задач } return sum; } } class MyThread { public Thread Thrd; int[] a; int answer; / Создать один объект типа SumArray для всех экземпляров класса MyThread. / static SumArray sa = new SumArray(); // Сконструировать новый поток. public MyThread(string name, int[] nums) { a = nums; Thrd = new Thread(this.Run); Thrd.Name = name; Thrd.Start(); // начать поток } // Начать выполнение нового потока. void Run() { Console.WriteLine(Thrd.Name + " начат."); // Заблокировать вызовы метода SumIt(). lock(sa) answer = sa.SumIt(a); Console.WriteLine("Сумма для потока " + Thrd.Name + " равна " + answer); Console.WriteLine(Thrd.Name + " завершен."); } } class Sync { static void Main() { int[] a = (1, 2, 3, 4, 5}; MyThread mt1 = new MyThread("Потомок #1", a); MyThread mt2 = new MyThread("Потомок #2", a); mt1.Thrd.Join(); mt2.Thrd.Join(); } } В данной программе блокируется вызов метода sa.SumIt(), а не сам метод SumIt(). Ниже приведена соответствующая строка кода, в которой осуществляется подобная блокировка. // Заблокировать вызовы метода SumIt(). lock(sa) answer = sa.SumIt(а); Объект sa является закрытым, и поэтому он может быть благополучно заблокиро ван. При таком подходе к синхронизации потоков данная программа дает такой же правильный результат, как и при первоначальном подходе. Класс Monitor и блокировка Ключевое слово lock на самом деде служит в C# быстрым способом доступа к средствам синхронизации, определенным в классе Monitor, который находится в про странстве имен System.Threading. В этом классе определен, в частности, ряд методов для управления синхронизацией. Например, для получения блокировки объекта вы зывается метод Enter(), а для снятия блокировки — метод Exit(). Ниже приведены общие формы этих методов: public static void Enter(object obj) public static void Exit(object obj) где obj обозначает синхронизируемый объект. Если же объект недоступен, то после вызова метода Enter() вызывающий поток ожидает до тех пор, пока объект не станет доступным. Тем не менее методы Enter() и Exit() применяются редко, поскольку оператор lock автоматически предоставляет эквивалентные средства синхронизации потоков. Именно поэтому оператор lock оказывается "более предпочтительным" для получения блокировки объекта при программировании на С#. Впрочем, один метод из класса Monitor может все же оказаться полезным. Это метод TryEnter(), одна из общих форм которого приведена ниже. public static bool TryEnter(object obj) Этот метод возвращает логическое значение true, если вызывающий поток полу чает блокировку для объекта obj, а иначе он возвращает логическое значение false. Но в любом случае вызывающему потоку придется ждать своей очереди. С помощью метода TryEnter() можно реализовать альтернативный вариант синхронизации по токов, если требуемый объект временно недоступен. Кроме того, в классе Monitor определены методы Wait(), Pulse() и PulseAll(), которые рассматриваются в следующем разделе. Сообщение между потоками с помощью методов Wait(), Pulse() и PulseAll() Рассмотрим следующую ситуацию. Поток Т выполняется в кодовом блоке lock, и ему требуется доступ к ресурсу R, который временно недоступен. Что же тогда делать потоку Т? Если поток Т войдет в организованный в той или иной форме цикл опроса, ожидая освобождения ресурса R, то тем самым он свяжет соответствующий объект, блокируя доступ к нему других потоков. Это далеко не самое оптимальное решение, поскольку оно лишает отчасти преимуществ программирования для многопоточной среды. Более совершенное решение заключается в том, чтобы временно освободить объект и тем самым дать возможность выполняться другим потокам. Такой подход основывается на некоторой форме сообщения между потоками, благодаря которому один поток может уведомлять другой о том, что он заблокирован и что другой поток может возобновить свое выполнение. Сообщение между потоками организуется в C# с помощью методов Wait(), Pulse() и PulseAll(). Методы Wait(), Pulse() и PulseAll() определены в классе Monitor и могут вызываться только из заблокированного фрагмента блока. Они применяются следую щим образом. Когда выполнение потока временно заблокировано, он вызывает метод Wait(). В итоге поток переходит в состояние ожидания, а блокировка с соответствую щего объекта снимается, что дает возможность использовать этот объект в другом по токе. В дальнейшем ожидающий поток активизируется, когда другой поток войдет в аналогичное состояние блокировки, и вызывает метод Pulse() или PulseAll(). При вызове метода Pulse() возобновляется выполнение первого потока, ожидающего сво ей очереди на получение блокировки. А вызов метода PulseAll() сигнализирует о снятии блокировки всем ожидающим потокам. Ниже приведены две наиболее часто используемые формы метода Wait(). public static bool Wait(object obj) public static bool Wait(object obj, int миллисекунд