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

Потоки

Контроль вторичных потоков. Callback-методы

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

Проблема заключается в том, КОГДА и КАКИМ ОБРАЗОМ о проделанной работе станет известно в первичном потоке.

Для анализа результата выполнения вторичного потока можно использовать метод класса, который обеспечивает запуск вторичного потока. Соответствующим образом настроенный делегат также может быть передан в качестве параметра конструктору вторичного потока. Вызывать метод класса, запустившего вторичный поток, можно будет по выполнении работ во ВТОРИЧНОМ потоке.

Таким образом, функция, контролирующая завершение работы вторичного потока, сама будет выполняться во ВТОРИЧНОМ потоке! Если при этом для дальнейшей работы первичного потока (который в этот момент, возможно, находится в состоянии ожидания) необходима информация о результатах проделанной вторичным потоком работы, контролирующая функция справится с этой задачей за счет того, что она имеет доступ ко всем данным и методам своего класса. И неважно, в каком потоке она при этом выполнялась:

using System;
 using System.Threading;

 // Класс WorkThread включает необходимую информацию,
 // метод и делегат для вызова метода, который запускается
 // после выполнения задачи.
 public class WorkThread 
 {
 // Входная информация.
 private string entryInformation;
 private int value;
 // Ссылка на объект - представитель класса-делегата, с помощью которого
 // вызывается метод обратного вызова. Сам класс-делегат объявляется позже.
 private CallbackMethod callback;

 // Конструктор получает входную информацию и настраивает
 // callback delegate.
 public WorkThread(string text, int number, 
CallbackMethod callbackDelegate) 
 {
 entryInformation = text;
 value = number;
 callback = callbackDelegate;
 }
  
 // Метод, обеспечивающий выполнение поставленной задачи:
 // составляет строку и после дополнительной проверки настройки
 // callback-делегата обеспечивает вызов метода.
 public void ThreadProc() 
 {
 Console.WriteLine(entryInformation, value);
 if (callback != null)
 callback(1); // Вот, вызвал ЧУЖОЙ МЕТОД в СВОЕМ потоке.
 }
 }

 // Класс-делегат задает сигнатуру callback-методу.
 //
 public delegate void CallbackMethod(int lineCount);
 // Entry Point for the example.
 //
 public class Example 
 {
 public static void Main() 
 {
 // Supply the state information required by the task.
 WorkThread tws = new WorkThread("This report displays the number {0}.",
                                  125,
                                  new CallbackMethod(ResultCallback));
 Thread t = new Thread(new ThreadStart(tws.ThreadProc));
 t.Start();
 Console.WriteLine("Первичный поток поработал. Теперь ждет.");
 t.Join();
 Console.WriteLine("Вторичный поток отработал. Главный поток остановлен."); 
 }

 // Callback-метод, естественно, соответствует сигнатуре
 // callback класса делегата.
 public static void ResultCallback(int lineCount) 
 {
 Console.WriteLine("Вторичный поток обработал {0} строк.", lineCount); 
 }
 }
Листинг 15.8.

Callback-метод – метод – член класса, запустившего вторичный поток. Этот метод запускается "в качестве уведомления" о том, что вторичный поток "завершил выполнение своей миссии". Особенность Callback-метода заключается в том, что он выполняется в "чужом" потоке.

Организация взаимодействия потоков

Посредством общедоступных (public) данных

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

using System;
 using System.Threading;

namespace CommunicatingThreadsData
 {

public delegate void CallBackFromStartClass (long param);
 // Данные. Предмет и основа взаимодействия двух потоков.
 class CommonData
 {
 public long lVal;
 public CommonData(long key)
 {
 lVal = key;	
 }	
 }


 // Классы Worker и Inspector: основа взаимодействующих потоков. 
class Worker
 {
 CommonData cd;
 // Конструктор...
 public Worker(ref CommonData rCDKey)
 {
 cd = rCDKey; 
 }
public void startWorker()
 {
 DoIt(ref cd);
 }

 // Тело рабочей функции...	
public void DoIt(ref CommonData cData)
 {//====================================
 for (;;)
 {
 cData.lVal++; // Изменили значение...
 Console.Write("{0,25}\r",cData.lVal); // Сообщили о результатах.
 }	
 }//====================================
 }

class Inspector
 {
 long stopVal;
 CommonData cd;
 CallBackFromStartClass callBack;

 // Конструктор... Подготовка делегата для запуска CallBack-метода. 
public Inspector
      (ref CommonData rCDKey, long key, CallBackFromStartClass cbKey)
 {
 stopVal = key;
 cd = rCDKey;
 callBack = cbKey;
 }

public void startInspector()
 {
 measureIt(ref cd);
 }


 // Тело рабочей функции...	
public void measureIt(ref CommonData cData)
 {//====================================
 for (;;)
 {
 if (cData.lVal < stopVal) 
 {
 Thread.Sleep(100);
 Console.WriteLine("\n{0,–25}",cData.lVal);
 }
 else
callBack(cData.lVal);
 }	
 }//====================================
 }


class StartClass
 {

static Thread th0, th1;
 static CommonData cd;
 static long result = 0;


static void Main(string[] args)
 {

StartClass.cd = new CommonData(0);
 // Конструкторы классов Worker и Inspector несут дополнительную нагрузку.
 // Они обеспечивают необходимыми значениями методы,
 // выполняемые во вторичных потоках.

Worker work;
 // До начала выполнения потока вся необходимая информация доступна методу.
 work = new Worker(ref cd);
 Inspector insp;
 // На инспектора возложена дополнительная обязанность
 // вызова функции-терминатора.
 // Для этого используется специально определяемый и настраиваемый делегат.
 insp = new Inspector(ref cd,
                      50000,
                      new CallBackFromStartClass(StartClass.StopMain));

 // Стартовые функции потоков должны соответствовать сигнатуре
 // класса делегата ThreadStart. Поэтому они не имеют параметров.
 ThreadStart t0, t1;
 t0 = new ThreadStart(work.startWorker);
 t1 = new ThreadStart(insp.startInspector);
 	
 // Созданы вторичные потоки.
 StartClass.th0 = new Thread(t0);
 StartClass.th1 = new Thread(t1);

 // Запущены вторичные потоки.
 StartClass.th0.Start();
 StartClass.th1.Start();

 // Еще раз о методе Join(): Выполнение главного потока приостановлено.
 StartClass.th0.Join();
 StartClass.th1.Join();

 // Потому последнее слово остается за главным потоком приложения.
 Console.WriteLine("Main(): All stoped at {0}. Bye.", result);

 }

 // Функция - член класса StartClass выполняется во ВТОРИЧНОМ потоке!
 public static void StopMain(long key)
 {
 Console.WriteLine("StopMain: All stoped at {0}...", key);
 // Остановка рабочих потоков. Ее выполняет функция - член
 // класса StartClass. Этой функции в силу своего определения
 // известно ВСЕ о вторичных потоках. Но выполняется она
 // в ЧУЖОМ (вторичном) потоке. Поэтому:
 // 1. надо предпринять особые дополнительные усилия для того, чтобы
 // результат работы потоков оказался доступен в главном потоке. 
 /*StartClass.*/result = key;
 // 2. очень важна последовательность остановки потоков,
 StartClass.th0.Abort();
 StartClass.th1.Abort();

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

Посредством общедоступных (public) свойств

Следующий вариант организации взаимодействия между потоками основан на использовании общедоступных свойств. От предыдущего примера отличается тем, что доступ к закрытому счетчику lVal в соответствии с принципами инкапсуляции осуществляется через свойство с блоками get (акцессор) и set (мутатор):

using System;
 using System.Threading;

namespace CommunicatingThreadsData
 {
 public delegate void CallBackFromStartClass (long param);
 // Данные. Предмет и основа взаимодействия двух потоков.
 class CommonData
 {
 private long lVal;
 public long lValProp
 {
 get
 {
 return lVal;
 }
 set
 {
 lVal = value;
 }
 }

public CommonData(long key)
 {
 lVal = key;	
 }	
 }


 // Классы Worker и Inspector: основа взаимодействующих потоков. 
class Worker
 {
 CommonData cd;
 // Конструктор умолчания...
 public Worker(ref CommonData rCDKey)
 {
 cd = rCDKey; 
 }
 public void startWorker()
 {
 DoIt(ref cd);
 }

 // Тело рабочей функции...	
public void DoIt(ref CommonData cData)
 {//====================================
 for (;;)
 {
 cData.lValProp++;
 Console.Write("{0,25}\r",cData.lValProp);
 }	
 }//====================================
 }

class Inspector
 {
 long stopVal;
 CommonData cd;
 CallBackFromStartClass callBack;

 // Конструктор...
 public 
    Inspector(ref CommonData rCDKey, long key, CallBackFromStartClass cbKey)
 {
 stopVal = key;
 cd = rCDKey;
 callBack = cbKey;
 }

public void startInspector()
 {
 measureIt(ref cd);
 }


 // Тело рабочей функции...	
public void measureIt(ref CommonData cData)
 {//====================================
 for (;;)
 {
 if (cData.lValProp < stopVal) 
 {
 Thread.Sleep(100);
 Console.WriteLine("\n{0,–25}",cData.lValProp);
 }
 else
callBack(cData.lValProp);
 }	
 }//====================================
 }


class StartClass
 {
 static Thread th0, th1;
 static CommonData cd;
 static long result = 0;

static void Main(string[] args)
 {
 StartClass.cd = new CommonData(0);
 // Конструкторы классов Worker и Inspector несут дополнительную нагрузку.
 // Они обеспечивают необходимыми значениями методы,
 // выполняемые во вторичных потоках.

Worker work;
 // До начала выполнения потока вся необходимая информация доступна методу.
 work = new Worker(ref cd);
 Inspector insp;
 // На инспектора возложена дополнительная обязанность
 // вызова функции-терминатора.
 // Для этого используется специально определяемый и настраиваемый 
 // делегат.
 insp = new Inspector
        (ref cd, 50000, new CallBackFromStartClass(StartClass.StopMain));
 // Стартовые функции потоков должны соответствовать сигнатуре
 // класса делегата ThreadStart. Поэтому они не имеют параметров.
 ThreadStart t0, t1;
 t0 = new ThreadStart(work.startWorker);
 t1 = new ThreadStart(insp.startInspector);
 // Созданы вторичные потоки.
 StartClass.th0 = new Thread(t0);
 StartClass.th1 = new Thread(t1);

 // Запущены вторичные потоки.
 StartClass.th0.Start();
 StartClass.th1.Start();

 // Еще раз о методе Join(): Выполнение главного потока приостановлено.
 StartClass.th0.Join();
 StartClass.th1.Join();

 // Потому последнее слово остается за главным потоком приложения.
 Console.WriteLine("Main(): All stoped at {0}. Bye.", result);
 }

 // Функция - член класса StartClass выполняется во ВТОРИЧНОМ потоке!
 public static void StopMain(long key)
 {
 Console.WriteLine("StopMain: All stoped at {0}...", key);
 // Остановка рабочих потоков. Ее выполняет функция - член
 // класса StartClass. Этой функции в силу своего определения
 // известно ВСЕ о вторичных потоках. Но выполняется она
 // в ЧУЖОМ (вторичном) потоке. Поэтому:
 // 1. надо предпринять особые дополнительные усилия для того, чтобы
 // результат работы потоков оказался доступен в главном потоке. 
 /*StartClass.*/result = key;
 // 2. очень важна последовательность остановки потоков,
 StartClass.th0.Abort();
 StartClass.th1.Abort();
 // Этот оператор не выполняется! Поток, в котором выполняется
 // метод - член класса StartClass StopMain(), остановлен.

Console.WriteLine("StopMain(): bye.");
 }
 }
 }
Листинг 15.10.

Посредством общедоступных очередей

Queue – класс, который представляет коллекцию объектов ( objects ), работающую по принципу "первым пришел, первым ушел" (first in, first out).

Stack – класс, который представляет коллекцию объектов ( objects ), работающую по принципу "последним пришел, первым ушел" (last in, first out).

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

Организация работы потоков по этой схеме предполагает:

  • выделение обрабатываемых данных в отдельный класс;
  • создание общедоступного объекта – представителя класса "Очередь" с интерфейсом, обеспечивающим размещение и извлечение данных;
  • разработку классов, содержащих методы генерации и обработки данных и реализующих интерфейс доступа к этим данным;
  • разработку методов обратного вызова, сообщающих о результатах выполнения задач генерации и обработки данных;
  • создание и запуск потока, обеспечивающего генерацию данных и размещение данных в очереди;
  • создание и запуск потока, обеспечивающего извлечение данных из очереди и обработку данных.

Пример:

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

 // Конструктор умолчания...
 public Receiver(ref Queue queueKey, CallBackFromStartClass cbKey)
 {
 cdQueue = queueKey; 
callBack = cbKey;
 }

public void startReceiver()
 {
 DoIt();
 }

 // Тело рабочей функции...	
public void DoIt()
 {
 CommonData cd = null;
 while (true)
 {

Console.WriteLine("Receiver. notifications in queue: {0}",cdQueue.Count);
 if (cdQueue.Count > 0)
 {
 cd = (CommonData)cdQueue.Dequeue();
 if (cd == null)
 Console.WriteLine("?????");
 else
 {
 Console.WriteLine("Process started ({0}).", cd.iValProp);
 // Выбрать какой-нибудь из способов обработки полученного уведомления.
 // Заснуть на соответствующее количество тиков.
 // Thread.Sleep(cd.iValProp);
 // Заняться элементарной арифметикой. С усыплением потока.
 while (cd.iValProp != 0)
 {
 cd.iValProp––;
 Thread.Sleep(cd.iValProp);
 Console.WriteLine("process:{0}",cd.iValProp);
 }
 }
 }
 else
callBack("Receiver");

Thread.Sleep(100);
 }	
 }
 }

class Sender
 {
 Random rnd;
 int stopVal;
 Queue cdQueue;
 CallBackFromStartClass callBack;

 // Конструктор...
 public Sender(ref Queue queueKey, int key, CallBackFromStartClass cbKey)
 {
 rnd = new Random(key);
 stopVal = key;
 cdQueue = queueKey;
 callBack = cbKey;
 }

public void startSender()
 {
 sendIt();
 }

 // Тело рабочей функции...	
public void sendIt()
 {//====================================
 	
while (true)
 {
 if (stopVal > 0) 
 {
 // Размещение в очереди нового члена со случайными характеристиками.
 cdQueue.Enqueue(new CommonData(rnd.Next(0,stopVal)));
 stopVal––;
 }
 else
callBack("Sender");

Console.WriteLine("Sender. in queue:{0}, the rest of notifications:{1}.",
 cdQueue.Count, stopVal);
 Thread.Sleep(100);
 }	
 }
 }

class StartClass
 {
 static Thread th0, th1;
 static Queue NotificationQueue;
 static string[] report = new string[2];

static void Main(string[] args)
 {
 StartClass.NotificationQueue = new Queue();
 // Конструкторы классов Receiver и Sender несут дополнительную нагрузку.
 // Они обеспечивают необходимыми значениями методы,
 // выполняемые во вторичных потоках.
 Sender sender;
 // По окончании работы отправитель вызывает функцию-терминатор.
 // Для этого используется специально определяемый и настраиваемый 
 // делегат.
 sender = new Sender(ref NotificationQueue,
 100,
 new CallBackFromStartClass(StartClass.StopMain));

Receiver receiver;
 // Выбрав всю очередь, получатель вызывает функцию-терминатор.
 receiver = new Receiver(ref NotificationQueue,
 new CallBackFromStartClass(StartClass.StopMain));
 // Стартовые функции потоков должны соответствовать сигнатуре
 // класса делегата ThreadStart. Поэтому они не имеют параметров.
 ThreadStart t0, t1;
 t0 = new ThreadStart(sender.startSender);
 t1 = new ThreadStart(receiver.startReceiver);

 // Созданы вторичные потоки.
 StartClass.th0 = new Thread(t0);
 StartClass.th1 = new Thread(t1);

 // Запущены вторичные потоки.
 StartClass.th0.Start();
 StartClass.th1.Start();
 // Еще раз о методе Join(): 
 // Выполнение главного потока приостановлено до завершения
 // выполнения вторичных потоков.
 StartClass.th0.Join();
 StartClass.th1.Join();
 // Потому последнее слово остается за главным потоком приложения.
 Console.WriteLine("Main(): " + report[0] + "..." + report[1] + "... 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("Receiver"))
 {
 report[1] = "Receiver all did.";
 StartClass.th1.Abort();
 }
 // Этот оператор не выполняется! Поток, в котором выполняется
 // метод - член класса StartClass StopMain(), остановлен.
 Console.WriteLine("StopMain(): bye.");
 }
 }
 }
Листинг 15.11.

Состязание потоков

В ранее рассмотренном примере временные задержки при генерации и обработке данных подобраны таким образом, что обработчик завершает свою деятельность последним. Таким образом, обеспечивается обработка ВСЕГО множества данных, размещенных в очереди. Разумеется, это идеальная ситуация. Изменение соответствующих значений может привести к тому, что обработчик данных опустошит очередь и завершит работу раньше, чем генератор данных разместит в очереди все данные.

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

Подобная ситуация хорошо известна как "Race conditions" – состязание потоков и должна учитываться при реализации многопоточных приложений. Результаты работы потока-обработчика не должны зависеть от быстродействия потока-генератора.

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