Текст книги "C# 4.0: полное руководство"
Автор книги: Герберт Шилдт
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 67 (всего у книги 83 страниц)
Нередко оказывается полезно знать, когда именно завершается поток. В предыдущих примерах программ для этой цели отслеживалось значение переменной Count
. Но ведь это далеко не лучшее и не совсем пригодное для обобщения решение. Правда, в классе Thread
имеются два других средства для определения момента окончания потока. С этой целью можно, прежде всего, опросить доступное только для чтения свойство IsAlive
, определяемое следующим образом.
public bool IsAlive { get; }
Свойство IsAlive
возвращает логическое значение true
, если поток, для которого оно вызывается, по-прежнему выполняется. Для «опробования» свойства IsAlive
подставьте приведенный ниже фрагмент кода вместо кода в классе MoreThread
из предыдущей версии многопоточной программы, как показано ниже.
// Использовать свойство IsAlive для отслеживания момента окончания потоков,
class MoreThreads {
static void Main() {
Console.WriteLine(«Основной поток начат.»);
// Сконструировать три потока.
MyThread mt1 = new MyThread(«Потомок #1»);
MyThread mt2 = new MyThread(«Потомок #2»);
MyThread mt3 = new MyThread(«Потомок #3»);
do {
Console.Write(".");
Thread.Sleep(100);
} while (mt1.Thrd.IsAlive &&
mt2.Thrd.IsAlive &&
mt3.Thrd.IsAlive);
Console.WriteLine(«Основной поток завершен.»);
}
}
При выполнении этой версии программы результат получается таким же, как и прежде. Единственное отличие заключается в том, что в ней используется свойство IsAlive
для отслеживания момента окончания порожденных потоков.
Еще один способ отслеживания момента окончания состоит в вызове метода Join()
. Ниже приведена его простейшая форма.
public void Join()
Метод Join()
ожидает до тех пор, пока поток, для которого он был вызван, не завершится. Его имя отражает принцип ожидания до тех пор, пока вызывающий поток не присоединится к вызванному методу. Если же данный поток не был начат, то генерируется исключение ThreadStateException
. В других формах метода Join()
можно указать максимальный период времени, в течение которого следует ожидать завершения указанного потока.
В приведенном ниже примере программы метод Join()
используется для того, чтобы основной поток завершился последним.
// Использовать метод Join().
using System;
using System.Threading;
class MyThread {
public int Count;
public Thread Thrd;
public MyThread(string name) {
Count = 0;
Thrd = new Thread(this.Run);
Thrd.Name = name;
Thrd.Start();
}
// Точка входа в поток,
void Run() {
Console.WriteLine(Thrd.Name + « начат.»);
do {
Thread.Sleep(500);
Console.WriteLine ("В потоке " + Thrd.Name +
",
Count = " + Count);
Count++;
} while(Count < 10);
Console.WriteLine(Thrd.Name + « завершен.»);
}
}
// Использовать метод Join() для ожидания до тех пор,
// пока потоки не завершатся,
class JoinThreads {
static void Main() {
Console.WriteLine(«Основной поток начат.»);
// Сконструировать три потока.
MyThread mt1 = new MyThread(«Потомок #1»);
MyThread mt2 = new MyThread(«Потомок #2»);
MyThread mt3 = new MyThread(«Потомок #3»);
mt1.Thrd.Join();
Console.WriteLine(«Потомок #1 присоединен.»);
mt2.Thrd.Join();
Console.WriteLine(«Потомок #2 присоединен.»);
mt3.Thrd.Join();
Console.WriteLine(«Потомок #3 присоединен.»);
Console.WriteLine(«Основной поток завершен.»);
}
}
Ниже приведен один из возможных результатов выполнения этой программы. Напомним, что он может отличаться в зависимости от среды выполнения, операционной системы и прочих факторов, влияющих на выполнение программы.
Основной поток начат.
Потомок #1 начат.
Потомок #2 начат.
Потомок #3 начат.
В потоке Потомок #1, Count = 0
В потоке Потомок #3, Count = 0
В потоке Потомок #2, Count = 0
В потоке Потомок #3, Count = 1
В потоке Потомок #2, Count = 1
В потоке Потомок #1, Count = 1
В потоке Потомок #1, Count = 2
В потоке Потомок #3, Count = 2
В потоке Потомок #2, Count = 2
В потоке Потомок #2, Count = 3
В потоке Потомок #3, Count = 3
В потоке Потомок #1, Count = 3
В потоке Потомок #3, Count = 4
В потоке Потомок #1, Count = 4
В потоке Потомок #2, Count = 4
В потоке Потомок #3, Count = 5
В потоке Потомок #1, Count = 5
В потоке Потомок #2, Count = 5
В потоке Потомок #2, Count = 6
В потоке Потомок #1, Count = 6
В потоке Потомок #3, Count = 6
В потоке Потомок #2, Count = 7
В потоке Потомок #1, Count = 7
В потоке Потомок #3, Count = 7
В потоке Потомок #2, Count = 8
В потоке Потомок #1, Count = 8
В потоке Потомок #3, Count = 8
В потоке Потомок #2, Count = 9
Потомок #2 завершен.
В потоке Потомок #1, Count = 9
В потоке Потомок #3, Count = 9
Потомок #3 завершен.
Потомок #1 завершен.
Потомок #1 присоединен.
Потомок #2 присоединен.
Потомок #3 присоединен.
Основной поток завершен.
Как видите, выполнение потоков завершилось после возврата из последовательного ряда вызовов метода Join().
Первоначально в среде .NET Framework нельзя было передавать аргумент потоку, когда он начинался, поскольку у метода, служившего в качестве точки входа в поток, не могло быть параметров. Если же потоку требовалось передать какую-то информацию, то к этой цели приходилось идти различными обходными путями, например использовать общую переменную. Но этот недостаток был впоследствии устранен, и теперь аргумент может быть передан потоку. Для этого придется воспользоваться другими формами метода Start()
, конструктора класса Thread
, а также метода, служащего в качестве точки входа в поток.
Аргумент передается потоку в следующей форме метода Start()
.
public void Start(object параметр)
Объект, указываемый в качестве аргумента параметр, автоматически передается методу, выполняющему роль точки входа в поток. Следовательно, для того чтобы передать аргумент потоку, достаточно передать его методу Start()
.
Для применения параметризированной формы метода Start()
потребуется следующая форма конструктора класса Thread
:
public Thread(ParameterizedThreadStart запуск)
где запуск обозначает метод, вызываемый с целью начать выполнение потока. Обратите внимание на то, что в этой форме конструктора запуск имеет тип ParameterizedThreadStart
, а не ThreadStart
, как в форме, использовавшейся в предыдущих примерах. В данном случае ParameterizedThreadStart
является делегатом, объявляемым следующим образом.
public delegate void ParameterizedThreadStart(object obj)
Как видите, этот делегат принимает аргумент типа object
. Поэтому для правильного применения данной формы конструктора класса Thread
у метода, служащего в качестве точки входа в поток, должен быть параметр типа object
.
В приведенном ниже примере программы демонстрируется передача аргумента потоку.
// Пример передачи аргумента методу потока.
using System;
using System.Threading;
class MyThread {
public int Count;
public Thread Thrd;
// Обратите внимание на то, что конструктору класса
// MyThread передается также значение типа int.
public MyThread(string name, int num) {
Count = 0;
// Вызвать конструктор типа ParameterizedThreadStart
// явным образом только ради наглядности примера.
Thrd = new Thread(this.Run);
Thrd.Name = name;
// Здесь переменная num передается методу Start()
// в качестве аргумента.
Thrd.Start(num);
}
// Обратите внимание на то, что в этой форме метода Run()
// указывается параметр типа object.
void Run(object num) {
Console.WriteLine(Thrd.Name + " начат со счета " + num);
do {
Thread.Sleep (500);
Console.WriteLine("В потоке " + Thrd.Name +
", Count = " + Count);
Count++;
} while(Count < (int) num);
Console.WriteLine(Thrd.Name + « завершен.»);
}
}
class PassArgDemo {
static void Main() {
// Обратите внимание на то, что число повторений
// передается этим двум объектам типа MyThread.
MyThread mt = new MyThread(«Потомок #1», 5);
MyThread mt2 = new MyThread(«Потомок #2», 3);
do {
Thread.Sleep(100);
} while (mt.Thrd.IsAlive ||
mt2.Thrd.IsAlive);
Console.WriteLine(«Основной поток завершен.»);
}
}
Ниже приведен результат выполнения данной программы, хотя у вас он может оказаться несколько иным.
Потомок #1 начат со счета 5
Потомок #2 начат со счета 3
В потоке Потомок #1, Count = 0
В потоке Потомок #2, Count = 0
В потоке Потомок #1, Count = 1
В потоке Потомок #2, Count = 1
В потоке Потомок #1, Count = 2
В потоке Потомок #2, Count = 2
Потомок #2 завершен.
В потоке Потомок #1, Count = 3
В потоке Потомок #1, Count = 4
Потомок #1 завершен.
Основной поток завершен.
Как следует из приведенного выше результата, первый поток повторяется пять раз, а второй – три раза. Число повторений указывается в конструкторе класса MyThread
и затем передается методу Run()
, служащему в качестве точки входа в поток, с помощью параметризированной формы ParameterizedThreadStart
метода Start()
.
Как упоминалось выше, в среде .NET Framework определены две разновидности потоков: приоритетный и фоновый. Единственное отличие между ними заключается в том, что процесс не завершится до тех пор, пока не окончится приоритетный поток, тогда как фоновые потоки завершаются автоматически по окончании всех приоритетных потоков. По умолчанию создаваемый поток становится приоритетным. Но его можно сделать фоновым, используя свойство IsBackground
, определенное в классе Thread
, следующим образом.
public bool IsBackground { get; set; }
Для того чтобы сделать поток фоновым, достаточно присвоить логическое значение true
свойству IsBackground
. А логическое значение false
указывает на то, что поток является приоритетным.
У каждого потока имеется свой приоритет, который отчасти определяет, насколько часто поток получает доступ к ЦП. Вообще говоря, низкоприоритетные потоки получают доступ к ЦП реже, чем высокоприоритетные. Таким образом, в течение заданного промежутка времени низкоприоритетному потоку будет доступно меньше времени ЦП, чем высокоприоритетному. Как и следовало ожидать, время ЦП, получаемое потоком, оказывает определяющее влияние на характер его выполнения и взаимодействия с другими потоками, исполняемыми в настоящий момент в системе.
Следует иметь в виду, что, помимо приоритета, на частоту доступа потока к ЦП оказывают влияние и другие факторы. Так, если высокоприоритетный поток ожидает доступа к некоторому ресурсу, например для ввода с клавиатуры, он блокируется, а вместо него выполняется низкоприоритетный поток. В подобной ситуации низкоприоритетный поток может получать доступ к ЦП чаще, чем высокоприоритетный поток в течение определенного периода времени. И наконец, конкретное планирование задач на уровне операционной системы также оказывает влияние на время ЦП, выделяемое для потока.
Когда порожденный поток начинает выполняться, он получает приоритет, устанавливаемый по умолчанию. Приоритет потока можно изменить с помощью свойства Priority
, являющегося членом класса Thread
. Ниже приведена общая форма данного свойства:
public ThreadPriority Priority{ get; set; }
где ThreadPriority
обозначает перечисление, в котором определяются приведенные ниже значения приоритетов.
ThreadPriority.Highest
ThreadPriority.AboveNormal
ThreadPriority.Normal
ThreadPriority.BelowNormal
ThreadPriority.Lowest
По умолчанию для потока устанавливается значение приоритета ThreadPriority.Normal
.
Для того чтобы стало понятнее влияние приоритетов на исполнение потоков, обратимся к примеру, в котором выполняются два потока: один с более высоким приоритетом. Оба потока создаются в качестве экземпляров объектов класса MyThread
. В методе Run()
организуется цикл, в котором подсчитывается определенное число повторений. Цикл завершается, когда подсчет достигает величины 1000000000 или когда статическая переменная stop
получает логическое значение true
. Первоначально переменная stop
получает логическое значение false
. В первом потоке, где производится подсчет до 1000000000, устанавливается логическое значение true
переменной stop
. В силу этого второй поток оканчивается на следующем своем интервале времени. На каждом шаге цикла строка в переменной currentName
проверяется на наличие имени исполняемого потока. Если имена потоков не совпадают, это означает, что произошло переключение исполняемых задач. Всякий раз, когда происходит переключение задач, имя нового потока отображается и присваивается переменной currentName
. Это дает возможность отследить частоту доступа потока к ЦП. По окончании обоих потоков отображается число повторений цикла в каждом из них.
// Продемонстрировать влияние приоритетов потоков.
using System;
using System.Threading;
class MyThread {
public int Count;
public Thread Thrd;
static bool stop = false;
static string currentName;
/* Сконструировать новый поток. Обратите внимание на то, что данный конструктор еще не начинает выполнение потоков. */
public MyThread(string name) {
Count = 0;
Thrd = new Thread(this.Run);
Thrd.Name = name; currentName = name;
}
// Начать выполнение нового потока,
void Run() {
Console.WriteLine("Поток " + Thrd.Name + « начат.»);
do {
Count++;
if(currentName != Thrd.Name) {
currentName = Thrd.Name;
Console.WriteLine("В потоке " + currentName);
}
} while(stop == false && Count < 1000000000);
stop = true;
Console.WriteLine("Поток " + Thrd.Name + « завершен.»);
}
}
class PriorityDemo {
static void Main() {
MyThread mt1 = new MyThread(«с высоким приоритетом»);
MyThread mt2 = new MyThread(«с низким приоритетом»);
// Установить приоритеты для потоков.
mt1.Thrd.Priority = ThreadPriority.AboveNormal;
mt2.Thrd.Priority = ThreadPriority.BelowNormal;
// Начать потоки,
mt1.Thrd.Start();
mt2.Thrd.Start() ;
mt1.Thrd.Join();
mt2.Thrd.Join();
Console.WriteLine();
Console.WriteLine("Поток " + mt1.Thrd.Name +
" досчитал до " + mt1.Count);
Console.WriteLine("Поток " + mt2.Thrd.Name +
" досчитал до " + mt2.Count);
}
}
Вот к какому результату может привести выполнение этой программы.
Поток с высоким приоритетом начат.
В потоке с высоким приоритетом
Поток с низким приоритетом начат.
В потоке с низким приоритетом
В потоке с высоким приоритетом
В потоке с низким приоритетом
В потоке с высоким приоритетом
В потоке с низким приоритетом
В потоке с высоким приоритетом
В потоке с низким приоритетом
В потоке с высоким приоритетом
В потоке с низким приоритетом
В потоке с высоким приоритетом
Поток с высоким приоритетом завершен.
Поток с низким приоритетом завершен.
Поток с высоким приоритетом досчитал до 1000000000
Поток с низким приоритетом досчитал до 23996334
Судя по результату, высокоприоритетный поток получил около 98% всего времени, которое было выделено для выполнения этой программы. Разумеется, конкретный результат может отличаться в зависимости от быстродействия ЦП и числа других задач, решаемых в системе, а также от используемой версии Windows.
Многопоточный код может вести себя по-разному в различных средах, поэтому никогда не следует полагаться на результаты его выполнения только в одной среде. Так, было бы ошибкой полагать, что низкоприоритетный поток из приведенного выше примера будет всегда выполняться лишь в течение небольшого периода времени до тех пор, пока не завершится высокоприоритетный поток. В другой среде высокоприоритетный поток может, например, завершиться еще до того, как низкоприоритетный поток выполнится хотя бы один раз.
Когда используется несколько потоков, то иногда приходится координировать действия двух или более потоков. Процесс достижения такой координации называется синхронизацией. Самой распространенной причиной применения синхронизации служит необходимость разделять среди двух или более потоков общий ресурс, который может быть одновременно доступен только одному потоку. Например, когда в одном потоке выполняется запись информации в файл, второму потоку должно быть запрещено делать это в тот же самый момент времени. Синхронизация требуется и в том случае, если один поток ожидает событие, вызываемое другим потоком. В подобной ситуации требуются какие-то средства, позволяющие приостановить один из потоков до тех пор, пока не произойдет событие в другом потоке. После этого ожидающий поток может возобновить свое выполнение.
В основу синхронизации положено понятие блокировки, посредством которой организуется управление доступом к кодовому блоку в объекте. Когда объект заблокирован одним потоком, остальные потоки не могут получить доступ к заблокированному кодовому блоку. Когда же блокировка снимается одним потоком, объект становится доступным для использования в другом потоке.
Средство блокировки встроено в язык С#. Благодаря этому все объекты могут быть синхронизированы. Синхронизация организуется с помощью ключевого слова lock
. Она была предусмотрена в C# с самого начала, и поэтому пользоваться ею намного проще, чем кажется на первый взгляд. В действительности синхронизация объектов во многих программах на C# происходит практически незаметно.
Ниже приведена общая форма блокировки:
lock(lockObj) {
// синхронизируемые операторы
}
где lockObj обозначает ссылку на синхронизируемый объект. Если же требуется синхронизировать только один оператор, то фигурные скобки не нужны. Оператор lock
гарантирует, что фрагмент кода, защищенный блокировкой для данного объекта, будет использоваться только в потоке, получающем эту блокировку. А все остальные потоки блокируются до тех пор, пока блокировка не будет снята. Блокировка снимается по завершении защищаемого ею фрагмента кода.
Блокируемым считается такой объект, который представляет синхронизируемый ресурс. В некоторых случаях им оказывается экземпляр самого ресурса или же произвольный экземпляр объекта, используемого для синхронизации. Следует, однако, иметь в виду, что блокируемый объект не должен быть общедоступным, так как в противном случае он может быть заблокирован из другого, неконтролируемого в программе фрагмента кода и в дальнейшем вообще не разблокируется. В прошлом для блокировки объектов очень часто применялась конструкция lock(this)
. Но она пригодна только в том случае, если this
является ссылкой на закрытый объект. В связи с возможными программными и концептуальными ошибками, к которым может привести конструкция lock(this)
, применять ее больше не рекомендуется. Вместо нее лучше создать закрытый объект, чтобы затем заблокировать его. Именно такой подход принят в примерах программ, приведенных далее в этой главе. Но в унаследованном коде C# могут быть обнаружены примеры применения конструкции lock(this)
. В одних случаях такой код оказывается безопасным, а в других – требует изменений во избежание серьезных осложнений при его выполнении.
В приведенной ниже программе синхронизация демонстрируется на примере управления доступом к методу SumIt()
, суммирующему элементы целочисленного массива.
// Использовать блокировку для синхронизации доступа к объекту.
using System;
using System.Threading;
class SumArray {
int sum;
object lockOn = new object(); // закрытый объект, доступный
// для последующей блокировки
public int SumIt(int[] nums) {
lock(lockOn) { // заблокировать весь метод
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 + « начат.»);
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 начат.
Потомок #2 начат.
Текущая сумма для потока Потомок #1 равна 1
Текущая сумма для потока Потомок #1 равна 3
Текущая сумма для потока Потомок #1 равна 6
Текущая сумма для потока Потомок #1 равна 10
Текущая сумма для потока Потомок #1 равна 15
Сумма для потока Потомок #1 равна 15
Потомок #1 завершен.
Текущая сумма для потока Потомок #2 равна 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 начат.
Потомок #2 начат.
Текущая сумма для потока Потомок #1 равна 1
Текущая сумма для потока Потомок #2 равна 1
Текущая сумма для потока Потомок #1 равна 3
Текущая сумма для потока Потомок #2 равна 5
Текущая сумма для потока Потомок #2 равна 11
Текущая сумма для потока Потомок #1 равна 8
Текущая сумма для потока Потомок #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.Sum It(), а не сам метод SumIt(). Ниже приведена соответствующая строка кода, в которой осуществляется подобная блокировка.
// Заблокировать вызовы метода SumIt().
lock(sa) answer = sa.SumIt(a);
Объект 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()
, которые рассматриваются в следующем разделе.