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

Потоки

Синхронизация работы потоков

Организация критических секций

// Пара потоков "наперегонки" заполняет одну очередь.
 // Эти потоки синхронизуются посредством критических секций кода,
 // связанных с разделяемым ресурсом – общей очередью. 
 // Третий поток читает из этой очереди.
 // Этот поток синхронизуется посредством монитора.
 // Методы Enter(...) и Exit(...) обеспечивают вход в критическую секцию 
 // кода, связанную с конкретным разделяемым объектом, и тем самым 
 // блокируют одновременное выполнение какого-либо связанного 
 // с данным ресурсом кода в другом потоке. 
 // Толчея потоков сопровождается генерацией исключений.


using System;
 using System.Threading;
 using System.Collections;

namespace CommunicatingThreadsQueue
 {

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;	
 }	
 }
 // Классы Sender и Receiver: основа взаимодействующих потоков. 
class Sender
 {
 Queue cdQueue;
 CallBackFromStartClass callBack;
 int threadIndex;

 // Конструктор...
 public Sender(ref Queue queueKey, CallBackFromStartClass cbKey,	int iKey)
 {
 cdQueue = queueKey;
 callBack = cbKey;
 threadIndex = iKey;
 }

public void startSender()
 {
 DoIt();
 }

 // Тело рабочей функции...	
public void DoIt()
 {//==============================================================
 Console.WriteLine("Sender{0}.DoIt()", threadIndex);
 int i;
for (i = 0; i < 100; i++)
 {//==============================================================
 try
 {
 lock(cdQueue.SyncRoot)
 {//______________________________________________________________
 Console.WriteLine("Sender{0}.", threadIndex);
 cdQueue.Enqueue(new CommonData(i));
 Console.WriteLine(">> Sender{0} >> {1}.", threadIndex,cdQueue.Count);
 foreach(CommonData cd in cdQueue)
 {
 Console.Write("\rS{0}:{1} ", threadIndex, cd.iValProp);
 }
 Console.WriteLine("__ Sender{0} __", threadIndex);
 }//______________________________________________________________
 }
 catch (ThreadAbortException e)
 {
 Console.WriteLine("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
 Console.WriteLine("AbortException from Sender{0}.", threadIndex); 
Console.WriteLine(e.ToString());
 Console.WriteLine("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
 }
 catch (Exception e)
 {
 Console.WriteLine("___________________________________________");
 Console.WriteLine("Exception from Sender{0}.", threadIndex); 
Console.WriteLine(e.ToString());
 Console.WriteLine("___________________________________________");
 callBack(threadIndex.ToString());
 }
 }//=============================================================	
callBack(string.Format("Sender{0}",threadIndex.ToString()));
 }//==============================================================
 }	

class Receiver
 {
 Queue cdQueue;
 CallBackFromStartClass callBack;
 int threadIndex;

 // Конструктор...
 public Receiver(ref Queue queueKey, CallBackFromStartClass cbKey, int iKey)
 {
 cdQueue = queueKey;
 callBack = cbKey;
 threadIndex = iKey;
 }

public void startReceiver()
 {
 DoIt();
 }

 // Тело рабочей функции...	
public void DoIt()
 {
 Console.WriteLine("Receiver.DoIt()");
 int i = 0;
 CommonData cd;
 while (i < 200)
 {	
try
 {
 Monitor.Enter(cdQueue.SyncRoot);
 Console.WriteLine("Receiver.");
 if (cdQueue.Count > 0)
 {
 cd = (CommonData)cdQueue.Dequeue();
 Console.WriteLine
 ("Receiver.current:{0},in queue:{1}.",cd.iValProp,cdQueue.Count);
 foreach(CommonData cdW in cdQueue)
 {
 Console.Write("\rR:{0}.", cdW.iValProp);
 }
 Console.WriteLine("__ Receiver __");
 i++;
 }
 Monitor.Exit(cdQueue.SyncRoot);
 }
 catch (ThreadAbortException e)
 {
 Console.WriteLine("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
 Console.WriteLine("AbortException from Receiver."); 
Console.WriteLine(e.ToString());	
Console.WriteLine("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
 }
 catch (Exception e)
 {
 Console.WriteLine("___________________________________________");
 Console.WriteLine("Exception from Receiver."); 
Console.WriteLine(e.ToString());	
Console.WriteLine("____________________________________________");
 callBack(threadIndex.ToString());
 }
 }
 callBack("Receiver");
 }	
 }

class StartClass
 {
 Thread th0, th1, th2;
 Queue queueX;
 string[] report = new string[3];
 ThreadStart t0, t1, t2;
 Sender sender0;
 Sender sender1;
 Receiver receiver;

static void Main(string[] args)
 {
 StartClass sc = new StartClass();
 sc.go();
 }

void go()
 {
 // Простая очередь.
 // queueX = new Queue();

 // Синхронизированная очередь. Строится на основе простой очереди.
 // Свойство синхронизированности дополнительно устанавливается в true
 // посредством метода Synchronized.
 queueX = Queue.Synchronized(new Queue());
 // Но на самом деле никакой разницы между двумя версиями очереди 
 // (между несинхронизированной очередью и синхронизирванной оболочкой 
 // вокруг несинхронизированной очереди) мною замечено не было. 
 // И в том и в другом
 // случае соответствующий код, который обеспечивает перебор 
 // элементов очереди, должен быть закрыт посредством lock-блока, 
 // с явным указанием ссылки на объект синхронизации.
 sender0 = new Sender(ref queueX, new CallBackFromStartClass(StopMain), 0);
 sender1 = new Sender(ref queueX, new CallBackFromStartClass(StopMain), 1);
 receiver = new Receiver(ref queueX, new CallBackFromStartClass(StopMain), 2);
 // Стартовые функции потоков должны соответствовать сигнатуре
 // класса делегата ThreadStart. Поэтому они не имеют параметров.
 t0 = new ThreadStart(sender0.startSender);
 t1 = new ThreadStart(sender1.startSender);
 t2 = new ThreadStart(receiver.startReceiver);
 // Созданы вторичные потоки.
 th0 = new Thread(t0);
 th1 = new Thread(t1);
 th2 = new Thread(t2);

th0.Start();
 th1.Start();
 th2.Start();

th0.Join();
 th1.Join();
 th2.Join();

Console.WriteLine
 ("Main(): " + report[0] + "..." + report[1] + "..." + report[2] + "... Bye.");
 }

 // Функция-член класса StartClass выполняется во ВТОРИЧНОМ потоке!
 public void StopMain(string param)
 {
 Console.WriteLine("StopMain: " + param);
 // Остановка рабочих потоков. Ее выполняет функция - член
 // класса StartClass. Этой функции в силу своего определения
 // известно ВСЕ о вторичных потоках. Но выполняется она
 // в ЧУЖИХ (вторичных) потоках.
 if (param.Equals("Sender0"))
 {
 report[0] = "Sender0 all did.";
 th0.Abort();
 }

if (param.Equals("Sender1"))
 {
 report[1] = "Sender1 all did.";
 th1.Abort();
 } 

if (param.Equals("Receiver"))
 {
 report[2] = "Receiver all did.";
 th2.Abort();
 }

if (param.Equals("0"))
 {
 th1.Abort();
 th2.Abort();	
th0.Abort();
 }

if (param.Equals("1"))
 {
 th0.Abort();
 th2.Abort();	
th1.Abort();
 }

if (param.Equals("2"))
 {
 th0.Abort();
 th1.Abort();
 th2.Abort();	
 }	

 // Этот оператор не выполняется! Поток, в котором выполняется
 // метод - член класса StartClass StopMain(), остановлен.
 Console.WriteLine("StopMain(): bye.");
 }
 }
 }
Листинг 15.13.

Специальные возможности мониторов

Класс Monitor управляет доступом к коду с использованием объекта синхронизации. Объект синхронизации предоставляет возможности для ограничения доступа к блоку кода, в общем случае обозначаемого как критическая секция.

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

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

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

Следующая таблица описывает действия, которые могут быть предприняты потоками при взаимодействии с монитором:

Действие Описание
Enter, TryEnter Закрытие секции с помощью объекта синхронизации. Это действие также обозначает начало критической секции. Никакие другие потоки не могут войти в заблокированную критическую секцию, если только они не используют другой объект синхронизации
Exit Освобождает блокировку критической секции кода. Также обозначает конец критической секции, связанной с данным объектом синхронизации
Wait Поток переходит в состояние ожидания, предоставляя тем самым другим потокам возможность выполнения других критических секций кода, связанных с данным объектом синхронизации.

В состоянии ожидания поток остается до тех пор, пока на выходе из другой секции, связанной с данным объектом синхронизации, другой поток не выполнит на мониторе действия Pulse ( PulseAll ), которые означают изменение состояния объекта синхронизации и обеспечивают выход потока из состояния ожидания на входе в критическую секцию

Pulse (signal), PulseAll Посылает сигнал ожидающим потокам. Сигнал служит уведомлением ожидающему потоку, что состояние объекта синхронизации изменилось и что владелец объекта готов его освободить. Находящийся в состоянии ожидания поток фактически находится в очереди для получения доступа к объекту синхронизации

Enter - и Exit -методы используются для обозначения начала или конца критической секции. Если критическая секция представляет собой "непрерывное" множество инструкций, закрытие кода посредством метода Enter гарантирует, что только один поток сможет выполнять код, закрытый объектом синхронизации.

Рекомендуется размещать эти инструкции в try block и помещать Exit instruction в finally -блоке.

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

// Синхронизация потоков с использованием класса монитора.
 // Монитор защищает очередь от параллельного вторжения со стороны 
 // взаимодействующих потоков из разных фрагментов кода.
 // Однако монитор не может защитить потоки от взаимной блокировки.
 // Поток просыпается, делает свою работу, будит конкурента, засыпает сам.
 // К тому моменту, как поток будит конкурента, конкурент должен спать. 
 // Активизация незаснувшего потока не имеет никаких последствий.
 // Если работающий поток разбудит не успевший заснуть поток – возникает
 // тупиковая ситуация. Оба потока оказываются погруженными в сон.
 // В этом случае имеет смысл использовать перегруженный вариант метода
 // Wait – с указанием временного интервала. 

using System;
 using System.Threading;
 using System.Collections;

namespace MonitorCS1
 {
 class MonitorApplication
 {
 const int MAX_LOOP_TIME = 100;
 Queue xQueue;

public MonitorApplication()
 {
 xQueue = new Queue();
 }

public void FirstThread()
 {
 int counter = 0;
 while(counter < MAX_LOOP_TIME)
 {
 Console.WriteLine("Thread_1___");
 counter++;
 Console.WriteLine("Thread_1...{0}", counter);

try
 {
 //Push element.
 xQueue.Enqueue(counter);

foreach(int ctr in xQueue)
 {
 Console.WriteLine(":::Thread_1:::{0}", ctr);	 
 }
 }
 catch (Exception ex)
 {
 Console.WriteLine(ex.ToString());
 }

 //Release the waiting thread. Применяется к конкурирующему потоку.
 lock(xQueue){Monitor.Pulse(xQueue);} 
Console.WriteLine(">1 Wait<");
 //Wait, if the queue is busy. Применяется к текущему потоку.
 // Собственное погружение в состояние ожидания.
 lock(xQueue){Monitor.Wait(xQueue,1000);}
 Console.WriteLine("!1 Work!");

 }
 Console.WriteLine("*****1 Finish*****");
 lock(xQueue) {Monitor.Pulse(xQueue);}

 }

public void SecondThread()
 {
 int counter = 0; 
while(counter < MAX_LOOP_TIME)
 {	
 //Release the waiting thread. Применяется к конкурирующему потоку.
 lock(xQueue){Monitor.Pulse(xQueue);}
 Console.WriteLine(">2 Wait<");
 // Собственное погружение в состояние ожидания.
 lock(xQueue){Monitor.Wait(xQueue,1000);}

Console.WriteLine("!2 Work!");
 Console.WriteLine("Thread_2___");

try
 {
 foreach(int ctr in xQueue)
 {
 Console.WriteLine(":::Thread_2:::{0}", ctr);	 
 }

 //Pop element.
 counter = (int)xQueue.Dequeue();
 }
 catch (Exception ex)
 {

counter = MAX_LOOP_TIME;
 Console.WriteLine(ex.ToString());
 }

Console.WriteLine("Thread_2...{0}",counter);
 }
 Console.WriteLine("*****2 Finish*****");	
lock(xQueue) {Monitor.Pulse(xQueue);}
 }

static void Main(string[] args)
 {
 // Create the MonitorApplication object.
 MonitorApplication test = new MonitorApplication(); 
Thread tFirst = new Thread(new ThreadStart(test.FirstThread));
 // Вторичные потоки созданы!
 Thread tSecond = new Thread(new ThreadStart(test.SecondThread));
 //Start threads.
 tFirst.Start();
 tSecond.Start();
 // Ждать завершения выполнения вторичных потоков.
 tFirst.Join();
 tSecond.Join(); 
 }
 }
 }
Листинг 15.14.

Mutex

Когда двум или более потокам нужно произвести доступ к разделяемому ресурсу одновременно, системе необходим механизм синхронизации для того, чтобы гарантировать использование ресурса только одним процессом. Класс Mutex — это примитив синхронизации, который предоставляет эксклюзивный доступ к разделяемому ресурсу только для одного процесса. Если поток получает семафор, второй поток, желающий получить этот семафор, приостанавливается до тех пор, пока первый поток не освободит семафор.

Можно использовать метод WaitHandle.WaitOne для запроса на владение семафором. Поток, владеющий семафором, может запрашивать его в повторяющихся вызовах Wait без блокировки выполнения. Однако поток должен вызвать метод ReleaseMutex соответственное количество раз для того, чтобы прекратить владеть семафором. Если поток завершается нормально во время владения семафором, состояние семафора задается сигнальным, и следующий ожидающий поток становится владельцем семафора. Если нет потоков, владеющих семафором, его состояние является сигнальным.

Открытые конструкторы
Mutex - конструктор Перегружен. Инициализирует новый экземпляр класса Mutex
Открытые свойства
Handle (унаследовано от WaitHandle ) Получает или задает собственный дескриптор операционной системы
Открытые методы
Close (унаследовано от WaitHandle ) При переопределении в производном классе освобождает все ресурсы, занимаемые текущим объектом WaitHandle
CreateObjRef (унаследовано от MarshalByRefObject ) Создает объект, который содержит всю необходимую информацию для разработки прокси-сервера, используемого для коммуникации с удаленными объектами
Equals (унаследовано от Object ) Перегружен. Определяет, равны ли два экземпляра Object
GetHashCode (унаследовано от Object ) Служит хэш-функцией для конкретного типа, пригоден для использования в алгоритмах хэширования и структурах данных, например в хэш-таблице
GetLifetimeService (унаследовано от MarshalByRefObject ) Извлекает служебный объект текущего срока действия, который управляет средствами срока действия данного экземпляра
GetType (унаследовано от Object ) Возвращает Type текущего экземпляра
InitializeLifetimeService (унаследовано от MarshalByRefObject ) Получает служебный объект срока действия для управления средствами срока действия данного экземпляра
ReleaseMutex Освобождает объект Mutex один раз
ToString (унаследовано от Object ) Возвращает String, который представляет текущий Object
WaitOne (унаследовано от WaitHandle ) Перегружен. В случае переопределения в производном классе, блокирует текущий поток до получения сигнала текущим объектом WaitHandle
Защищенные методы
Dispose (унаследовано от WaitHandle ) При переопределении в производном классе отключает неуправляемые ресурсы, используемые WaitHandle, и по возможности освобождает управляемые ресурсы
Finalize (унаследовано от WaitHandle ) Переопределен. Освобождает ресурсы, удерживаемые текущим экземпляром.

В языках C# и C++ для функций финализации используется синтаксис деструктора

MemberwiseClone (унаследовано от Object ) Создает неполную копию текущего объекта object

Многопоточное приложение. Способы синхронизации

Приводимый ниже пример является многопоточным приложением, пара дополнительных потоков которого получают доступ к одному и тому же объекту (объекту синхронизации). Результаты воздействия образующих потоки операторов наглядно проявляются на экране консольного приложения.

using System;
 using System.Threading;

namespace threads12
 {
 class TextPresentation
 {
 public Mutex mutex;

public TextPresentation()
 {
  mutex = new Mutex();
 }

public void showText(string text)
 {
 int i;
 // Объект синхронизации в данном конкретном случае – 
 // представитель класса TextPresentation. Для его обозначения 
 // используется первичное выражение this.__________________________
 //1. Блокировка кода монитором (начало) // Monitor.Enter(this);
 //2. Критическая секция кода (начало)// lock(this) {
 mutex.WaitOne();//3.Блокировка кода мьютексом (начало)//
 Console.WriteLine("\n" + (char)31 + (char)31 + (char)31 + (char)31);
 for (i = 0; i < 250; i++)
 {
 Console.Write(text);
 }
 Console.WriteLine("\n" + (char)30 + (char)30 + (char)30 + (char)30);
 mutex.ReleaseMutex();//3.Блокировка кода мьютексом (конец)//
 //2. Критическая секция кода (конец) // }
 //1. Блокировка кода монитором (конец) // Monitor.Exit(this);
 }
 }

class threadsRunners
 {

public static TextPresentation tp = new TextPresentation();
 public static void Runner1()
 {
 Console.WriteLine("thread_1 run!");
 Console.WriteLine("thread_1 – calling TextPresentation.showText");
 tp.showText("*");
 Console.WriteLine("thread_1 stop!");
 }

public static void Runner2()
 {
 Console.WriteLine("thread_2 run!");
 Console.WriteLine("thread_2 – calling TextPresentation.showText");
 tp.showText("|");
 Console.WriteLine("thread_2 stop!");
 }

static void Main(string[] args)
 {
 ThreadStart runner1 = new ThreadStart(Runner1);
 ThreadStart runner2 = new ThreadStart(Runner2);

Thread th1 = new Thread(runner1);
 Thread th2 = new Thread(runner2);

th1.Start();
 th2.Start();
 }
 }
 }
Листинг 15.15.

Рекомендации по недопущению блокировок потоков

  • Соблюдать определенный порядок при выделении ресурсов.
  • При освобождении выделенных ресурсов придерживаться обратного (reverse) порядка.
  • Минимизировать время неопределенного ожидания выделяемого ресурса.
  • Не захватывать ресурсы без необходимости и при первой возможности освобождать захваченные ресурсы.
  • Захватывать ресурс только в случае крайней необходимости.
  • В случае, если ресурс не удается захватить, повторную попытку его захвата производить только после освобождения ранее захваченных ресурсов.
  • Максимально упрощать структуру задачи, решение которой требует захвата ресурсов. Чем проще задача – тем на меньший период захватывается ресурс.
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