Российский Государственный Технологический Университет им. К.Э. Циолковского
Опубликован: 02.03.2007 | Доступ: свободный | Студентов: 5678 / 802 | Оценка: 3.96 / 3.45 | Длительность: 27:04:00
ISBN: 978-5-9556-0086-4
Лекция 15:

Потоки

Блокировки и тупики

Блокировка выполнения потока возникает при совместном использовании потоками нескольких ресурсов. В условиях, когда выполнение потоков явным образом не управляется, поток в нужный момент может не получить доступа к требуемому ресурсу, поскольку именно сейчас этот ресурс используется другим потоком.

Тупик – взаимная блокировка потоков:

поток 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.

Перечисление в коллекции в действительности не является потокобезопасной процедурой. Даже при синхронизации коллекции другие потоки могут изменить ее, что приводит к созданию исключения при перечислении. Чтобы обеспечить потокобезопасность при перечислении, можно либо заблокировать коллекцию на все время перечисления, либо перехватывать исключения, которые возникают в результате изменений, внесенных другими потоками.

kewezok kewezok
kewezok kewezok
Елена Шляхт
Елена Шляхт
Объясните плиз в чем отличие а++ от ++а
Почему результат разный?
int a=0, b=0;
Console.WriteLine(a++); //0
Console.WriteLine(++b); //1
a++;
++b;
Console.WriteLine(a); //2
Console.WriteLine(b); //2