Потоки
Блокировки и тупики
Блокировка выполнения потока возникает при совместном использовании потоками нескольких ресурсов. В условиях, когда выполнение потоков явным образом не управляется, поток в нужный момент может не получить доступа к требуемому ресурсу, поскольку именно сейчас этот ресурс используется другим потоком.
Тупик – взаимная блокировка потоков:
поток 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.
Перечисление в коллекции в действительности не является потокобезопасной процедурой. Даже при синхронизации коллекции другие потоки могут изменить ее, что приводит к созданию исключения при перечислении. Чтобы обеспечить потокобезопасность при перечислении, можно либо заблокировать коллекцию на все время перечисления, либо перехватывать исключения, которые возникают в результате изменений, внесенных другими потоками.