Потоки
Блокировки и тупики
Блокировка выполнения потока возникает при совместном использовании потоками нескольких ресурсов. В условиях, когда выполнение потоков явным образом не управляется, поток в нужный момент может не получить доступа к требуемому ресурсу, поскольку именно сейчас этот ресурс используется другим потоком.
Тупик – взаимная блокировка потоков:
поток A захватывает ресурс a и не может получить доступа к ресурсу b, занятому потоком B, который может быть освобожден потоком только по получению доступа к ресурсу a.
Пример взаимодействующих потоков рассматривается далее:
// Взаимодействующие потоки разделяют общие ресурсы – пару очередей. // Для успешной работы каждый поток должен последовательно получить // доступ к каждой из очередей. Из одной очереди взять, в другую // положить. Поток оказывается заблокирован, когда одна из очередей // оказывается занятой другим потоком. using System; using System.Threading; using System.Collections; namespace CommunicatingThreadsQueue { // Модифифицированный вариант очереди – очередь с флажком. // Захвативший эту очередь поток объявляет очередь "закрытой". public class myQueue: Queue { private bool isFree; public bool IsFree { get { return isFree; } set { isFree = value; } } public object myDequeue() { if (IsFree) {IsFree = false; return base.Dequeue();} else return null; } public bool myEnqueue(object obj) { if (IsFree == true) {base.Enqueue(obj); return true;} else return false; } } public delegate void CallBackFromStartClass (string param); // Данные. Предмет и основа взаимодействия двух потоков. class CommonData { private int iVal; public int iValProp { get { return iVal; } set { iVal = value; } } public CommonData(int key) { iVal = key; } } // Классы Receiver и Sender: основа взаимодействующих потоков. class Receiver { myQueue cdQueue0; myQueue cdQueue1; CallBackFromStartClass callBack; int threadIndex; // Конструктор... public Receiver(ref myQueue queueKey0, ref myQueue queueKey1, CallBackFromStartClass cbKey, int iKey) { threadIndex = iKey; if (threadIndex == 0) { cdQueue0 = queueKey0; cdQueue1 = queueKey1; } else { cdQueue1 = queueKey0; cdQueue0 = queueKey1; } callBack = cbKey; } public void startReceiver() { DoIt(); } // Тело рабочей функции... public void DoIt() { CommonData cd = null; while (true) { if (cdQueue0.Count > 0) { while (true) { cd = (CommonData)cdQueue0.myDequeue(); if (cd != null) break; Console.WriteLine(">> Receiver{0} is blocked.", threadIndex); } // Временная задержка "на обработку" полученного блока информации // влияет на частоту и продолжительность блокировок. Thread.Sleep(cd.iValProp*100); // И это не ВЗАИМНАЯ блокировка потоков. // "Обработали" блок – открыли очередь. // И только потом предпринимается попытка // обращения к очереди оппонента. cdQueue0.IsFree = true; //Записали результат во вторую очередь. while (cdQueue1.myEnqueue(cd) == false) { Console.WriteLine("<< Receiver{0} is blocked.", threadIndex); } // А вот если пытаться освободить захваченную потоком очередь // в этом месте – взаимной блокировки потоков не избежать! // cdQueue0.IsFree = true; // Сообщили о состоянии очередей. Console.WriteLine("Receiver{0}...{1}>{2}",threadIndex.ToString(), cdQueue0.Count,cdQueue1.Count); } else { cdQueue0.IsFree = true; callBack(string.Format("Receiver{0}",threadIndex.ToString())); } } } } class Sender { Random rnd; int stopVal; myQueue cdQueue0; myQueue cdQueue1; CallBackFromStartClass callBack; // Конструктор... public Sender(ref myQueue queueKey0, ref myQueue queueKey1, int key, CallBackFromStartClass cbKey) { rnd = new Random(key); stopVal = key; cdQueue0 = queueKey0; cdQueue1 = queueKey1; callBack = cbKey; } public void startSender() { sendIt(); } // Тело рабочей функции... public void sendIt() {//==================================== cdQueue0.IsFree = false; cdQueue1.IsFree = false; while (true) { if (stopVal > 0) { // Размещение в очереди нового члена со случайными характеристиками. cdQueue0.Enqueue(new CommonData(rnd.Next(0,stopVal))); cdQueue1.Enqueue(new CommonData(rnd.Next(0,stopVal))); stopVal––; } else { cdQueue0.IsFree = true; cdQueue1.IsFree = true; callBack("Sender"); } Console.WriteLine ("Sender. The rest of notifications: {0}, notifications in queue:{1},{2}.", stopVal, cdQueue0.Count, cdQueue1.Count); } }//==================================== } class StartClass { static Thread th0, th1, th2; static myQueue NotificationQueue0; static myQueue NotificationQueue1; static string[] report = new string[3]; static void Main(string[] args) { StartClass.NotificationQueue0 = new myQueue(); StartClass.NotificationQueue1 = new myQueue(); // Конструкторы классов Receiver и Sender несут дополнительную нагрузку. // Они обеспечивают необходимыми значениями методы, // выполняемые во вторичных потоках. Sender sender; // По окончании работы отправитель вызывает функцию-терминатор. // Для этого используется специально определяемый и настраиваемый делегат. sender = new Sender(ref NotificationQueue0, ref NotificationQueue1, 10, new CallBackFromStartClass(StartClass.StopMain)); Receiver receiver0; // Выбрав всю очередь, получатель вызывает функцию-терминатор. receiver0 = new Receiver(ref NotificationQueue0, ref NotificationQueue1, new CallBackFromStartClass(StartClass.StopMain),0); Receiver receiver1; // Выбрав всю очередь, получатель вызывает функцию-терминатор. receiver1 = new Receiver(ref NotificationQueue0, ref NotificationQueue1, new CallBackFromStartClass(StartClass.StopMain),1); // Стартовые функции потоков должны соответствовать сигнатуре // класса делегата ThreadStart. Поэтому они не имеют параметров. ThreadStart t0, t1, t2; t0 = new ThreadStart(sender.startSender); t1 = new ThreadStart(receiver0.startReceiver); t2 = new ThreadStart(receiver1.startReceiver); // Созданы вторичные потоки. StartClass.th0 = new Thread(t0); StartClass.th1 = new Thread(t1); StartClass.th2 = new Thread(t2); // Запущены вторичные потоки. StartClass.th0.Start(); // Еще раз о методе Join(): // Выполнение главного потока приостановлено до завершения // выполнения вторичного потока загрузки очередей. // Потоки получателей пока отдыхают. StartClass.th0.Join(); // Отработал поток загрузчика. // Очередь получателей. StartClass.th1.Start(); StartClass.th2.Start(); // Метод Join(): // Выполнение главного потока опять приостановлено // до завершения выполнения вторичных потоков. StartClass.th1.Join(); StartClass.th2.Join(); // Последнее слово остается за главным потоком приложения. // Но только после того, как отработают терминаторы. Console.WriteLine ("Main():"+report[0]+". "+report[1]+". "+report[2]+". Bye."); } // Функция - член класса StartClass выполняется во ВТОРИЧНОМ потоке! public static void StopMain(string param) { Console.WriteLine("StopMain: " + param); // Остановка рабочих потоков. Ее выполняет функция - член // класса StartClass. Этой функции в силу своего определения // известно ВСЕ о вторичных потоках. Но выполняется она // в ЧУЖИХ (вторичных) потоках. if (param.Equals("Sender")) { report[0] = "Sender all did."; StartClass.th0.Abort(); } if (param.Equals("Receiver0")) { report[1] = "Receiver0 all did."; StartClass.th1.Abort(); } if (param.Equals("Receiver1")) { report[2] = "Receiver1 all did."; StartClass.th2.Abort(); } // Этот оператор не выполняется! Поток, в котором выполняется // метод - член класса StartClass StopMain(), остановлен. Console.WriteLine("StopMain(): bye."); } } }Листинг 15.12.
Безопасность данных и критические секции кода
Некоторое значение, связанное с конкретным объектом, подвергается воздействию (изменению, преобразованию) со стороны потока. Это означает, что по отношению к объекту (значению объекта) применяется фиксированная последовательность операторов, в результате которой происходит КОРРЕКТНОЕ изменение состояния объекта или его значения.
В многопоточном приложении один и тот же объект может быть подвергнут одновременному "параллельному" воздействию со стороны нескольких потоков. Подобное воздействие представляет опасность для объекта и его значения, поскольку в этом случае порядок применения операторов из нескольких потоков (пусть даже и содержащих одни и те же операторы) неизбежно будет изменен.
В многопоточном программировании последовательности операторов, составляющих поток и при неконтролируемом доступе к объекту, возможно, приводящих к некорректному изменению состояния объекта, называются критическими секциями кода.
Управление последовательностью доступа потоков к объекту называют синхронизацией потоков.
Сам же объект называют объектом синхронизации.
Типичными средствами синхронизации потоков являются:
- критические секции,
- мониторы,
- мьютексы.
Очередь как объект синхронизации
Очередь — достаточно сложное образование с множеством свойств и методов, предназначенное для упорядоченного размещения объектов (все дети класса object ). Одновременное воздействие на очередь со стороны кода нескольких потоков представляет серьезную опасность — и не столько для самого объекта очереди в смысле возможного искажения сохраняемой в ней информации, сколько для самого приложения. Класс Queue взаимодействует с окружением через интерфейсы, а неупорядоченное воздействие на объект очереди через эти интерфейсы возбуждает исключения. Очередь располагает специальными средствами, позволяющими защитить объект от неупорядоченного воздействия со стороны множества потоков. Назначение некоторых средств и их применение очевидно, как использовать другие средства – пока неясно (я пометил их вопросительным знаком).
В класс входят методы и свойства:
- множество вариантов конструкторов – Queue(...) ;
- методы, обеспечивающие загрузку и выгрузку данных (объектов – представителей классов-наследников класса object ) – void Enqueue (object), object Dequeue() ;
- методы поиска – bool Contains(object) ;
- методы предъявления – object Peek() (возвращает объект из начала очереди, не удаляя его из очереди);
- свойство Count – сохраняет информацию о количестве объектов в очереди;
- свойство SyncRoot – предоставляет ссылку на ОБЪЕКТ СИНХРОНИЗАЦИИ, который используется при синхронизации потоков многопоточного приложения;
- свойство IsSynchronized – предоставляет информацию о том, синхронизирован ли объект для работы в многопоточном приложении. Это всего лишь значение объявленной в классе Queue булевской переменной;
- статический метод Synchronized – создающий синхронизированную оболочку вокруг объекта очереди.
Примеры использования очередей в приложении приводятся ниже, а пока – вопросы, связанные с взаимодействием объекта очереди с потоками многопоточного приложения.
Перебор элементов очереди посредством оператора цикла foreach – самое "опасное" для очереди занятие в условиях многопоточного приложения. И причина всех неприятностей заключается во внутреннем устройстве и особенностях реализации цикла foreach, который при своем выполнении использует множество функций интерфейса очереди:
Queue myCollection = new Queue(); :::::::::::::::::::::::::::::::::: // Перебор элементов очереди – критическая секция кода. foreach ( Object item in myCollection ) { ::::::::::::::::::::::::::::::::::::::: }
Возможный способ преодоления опасной ситуации – защита кода критической секцией. Суть защиты сводится к следующему. Поток, который выполняет собственный код при "подходе" к критической секции, связанной с конкретным объектом синхронизации, блокируется, если ЭТУ или ДРУГУЮ связанную с данным объектом синхронизации критическую секцию в данное время выполняет другой поток.
Queue myCollection = new Queue(); :::::::::::::::::::::::::::::::::::::: lock( myCollection.SyncRoot ) {// Критическая секция, которая связана с объектом // синхронизации, полученным от очереди // myCollection, обозначена... foreach ( Object item in myCollection ) { ::::::::::::::::::::::::::::::::::::::: } }
Пример синхронизации объекта очереди. Видно, как создавать синхронизированную оболочку вокруг несинхронизированной очереди, как узнавать о том, синхронизирована она или нет, НО ЗАЧЕМ ДЕЛАТЬ ЭТО – не сказано и не показано. Дело в том, что синхронизирована она или нет, а соответствующий код (критические секции кода) защищать все равно надо!
using System; using System.Collections; public class SamplesQueue { public static void Main() { // Creates and initializes a new Queue. Queue myQ = new Queue(); myQ.Enqueue( "The" ); myQ.Enqueue( "quick" ); myQ.Enqueue( "brown" ); myQ.Enqueue( "fox" ); // Creates a synchronized wrapper around the Queue. Queue mySyncdQ = Queue.Synchronized( myQ ); // Displays the sychronization status of both Queues. Console.WriteLine("myQ is {0}.", myQ.IsSynchronized ? "synchronized" : "not synchronized" ); Console.WriteLine( "mySyncdQ is {0}.", mySyncdQ.IsSynchronized ? "synchronized" : "not synchronized" ); } } //This code produces the following output. //myQ is not synchronized. //mySyncdQ is synchronized.
Стек как объект синхронизации
Stack – класс, который представляет коллекцию объектов, обслуживаемую по принципу "последним пришел — первым вышел".
Список всех членов этого типа представлен в разделе "Stack-члены".
public class Stack : ICollection, IEnumerable, ICloneable
Открытые статические (Shared в Visual Basic) члены этого типа могут использоваться для многопоточных операций. Потокобезопасность членов экземпляра не гарантируется.
Для обеспечения потокобезопасности операций с классом Stack, все они должны выполняться с помощью обертки, возвращенной методом Synchronized.
Перечисление в коллекции в действительности не является потокобезопасной процедурой. Даже при синхронизации коллекции другие потоки могут изменить ее, что приводит к созданию исключения при перечислении. Чтобы обеспечить потокобезопасность при перечислении, можно либо заблокировать коллекцию на все время перечисления, либо перехватывать исключения, которые возникают в результате изменений, внесенных другими потоками.