Глава 9. Класс BackgroundWorker и новые возможности работа с потоками в Visual Studio 2005/2008

Аннотация: В основу данной главы должны были лечь многочисленные материалы о программной реализации многопоточности, собираемые автором и опробованные на практике на протяжении нескольких лет. Но, поскольку и у автора, и в Интернете, материалов по данной теме становилось все больше, то все меньше оставалось смысла публиковать накопленное. Joseph Albahari подвел итог накопительству, разложив по полочкам и систематизировав все, что только возможно по данной теме. Но в Visual Studio .Net 2005 появился контрол BackgroundWorker (есть он и в Studio 2008), о возможностях применения которого еще далеко не все сказано. Рассмотрению некоторых из этих возможностей и посвящен данный материал.

Приложение может запускать один или несколько процессов в памяти, каждый из которых может выполняться как одно поточный или как многопоточный. Многопоточность или одновременное параллельное выполнение нескольких ветвей процесса (последовательности машинных команд или потоков) обеспечивает ОС. Это достигается выделением каждому потоку определенного кванта времени, по истечении которого управление может быть передано другому, ожидающему или временно блокированному (взаимное вытеснение потоков). В многопроцессорных системах к переключениям между потоками по квантам времени добавляется возможность распределения места выполнения потоков - на различных процессорах (параллелизм). Механизмы квантования и параллелизма лежат в основе многозадачности ОС. Основная необходимость использования многопоточности в пределах одного приложения возникает обычно тогда, когда при обмене большими объемами информации с некоторыми объектами (базами данных, серверами автоматизации...) приложение на период обмена перестает реагировать на действия извне (не может обрабатывать сообщения клавиатуры и мыши и создается ощущение, что программа зависла). Многопоточность может быть полезна и при выполнении длительных вычислений в фоновом режиме, при ожидании окончания ввода информации с клавиатуры (с внешних носителей) и в ряде других случаев, когда приложению есть чем заняться на период ожидания.


В начало

Параграф 1. Потоковые апартаменты и Windows Forms

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

Технология .NET свободна от старых потоковых моделей, однако к ним приходится возвращаться при работе с WinApi функциями в Windows Forms и Com-объектами (технология COM появился до того, как в Windows появилась многопоточность и многие ранние Com объекты не приспособлены для работы в многопоточном режиме). По этой причине возможны ситуации, такие как, например, когда при параллельном вызове одного и того же метода объекта, равно как и обращении к статической или глобальной переменной, один поток модифицировал объект (переменную) - другой, получив квант времени, восстановил свои значения. Для исключения подобных ситуаций появилась технология использования Apartments (потоковых апартаментов) и механизмы синхронизации потоков (о них речь пойдет в параграфе 3).

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

Базовые положения аpartments:

  • Каждому процессу сопоставляется один или несколько апартаментов.

  • Каждый поток живет в определенном апартаменте.

  • Каждый объект живет в определенном апартаменте.

Имеется три типа апартаментов:

  • STA (Single-Threaded Apartment - одно поточный). В одном процессе может иметься несколько апартаментов типа STA. В каждом STA живет только один поток. В STA живут объекты, не поддерживающие параллельный вызов своих методов.

  • MTA (Multi-Threaded Apartment - многопоточный). В одном процессе может быть только один MTA. В MTA живут объекты, поддерживающие параллельный вызов своих методов. С MTA связан, так называемый пул потоков.
       Когда .NET приложение стартует в среде Windows, CLR создает внутренний пул потоков, используемый средой для реализации асинхронных операций. Потоки могут добавляться в пул и удаляться из него по мере необходимости. Пул потоков управляется операционной системой. Для взаимодействия с пулом потоков предусмотрен класс ThreadPool, объект которого создается CLR при запуске приложения. Все домены приложений в рамках одного процесса используют общий пул потоков.
       При поступлении в MTA нового вызова из пула потоков выбирается поток для выполнения этого вызова (вызова некоторого метода объекта, который живет в данном апартаменте).

  • NA (Neutral Apartment - нейтральный). В одном процессе может иметься только один NA. В NA живут объекты, поддерживающие параллельный вызов своих методов. Потоки не связаны с NA. Любой поток из STA или MTA может временно покинуть свой апартамент и заняться выполнением некоторого метода некоторого объекта, живущего в NA.

Когда объект создается. то он соотносится с определенным апартаментом и потоком или потоками и остается связан с ними на весь период своего существования. В какой именно тип апартамента (STA, MTA или NA) может быть помещен объект, определяет разработчик объекта (информация хранится в реестре). Объекты, входящие в какой-либо апартамент, могут быть вызваны только потоком этого апартамента. Когда поток и объект находятся в одном апартаменте, то вызов методов объекта и безопасность данных понятны - до выполнения одного вызова поток не может перейти к другому вызову. Вызов же метода объекта одного апартамента из другого гораздо сложнее, т.к. прямые вызовы через границы апартаментов запрещены (объект может не поддерживать параллельные вызовы). Для данного случая предусмотрен специальный механизм "прокси/стаб". Суть метода - при вызове объекта из STA сам вызов преобразуется в сообщение, которое становится в очередь сообщений, связанную с вызываемым STA. Механизм подготовки и выполнения запросов получил название маршалинга. Маршалинг выполняется автоматически и реализуется через передачу сообщений в Windows Forms. Этот механизм также постоянно следит за событиями клавиатуры и мыши. Если обработка не успевает за новыми сообщениями, создается очередь сообщений, обрабатываемая в порядке поступления. В случае вызова объекта из MTA проблема решается проще - из пула связанных с MTA рабочих потоков выбирается произвольный поток. При использовании NA, как отмечено выше, любой поток из STA или MTA может временно покинуть свой апартамент и заняться выполнением некоторого метода некоторого объекта, живущего в NA.

Более подробное рассмотрение данного вопроса выходит за рамки темы раздела. Глубокую проработку данного материала можно найти в книге Владимира Добрынина "Технологии компонентного программирования"

Прежде чем перейти к технической стороне реализации, вспомним, что:

1. Апартаменты не используются когда выполняется "чистый" .NET код.

2. Апартамент предоставляется .Net коду только при выполнении WinAPI функций или использовании Com объектов.

По умолчанию .Net код будет сопоставлен MTA апартаменту, пока NA или STA апартамент не будет установлен принудительно.

Thread t = new Thread (...);
t.SetApartmentState (ApartmentState.STA);

Другой способ задания типа апартамента - использование атрибутов STAThread и MTAThread перед определением функции потока. Атрибут STAThread по умолчанию установлен для функции main в Windows приложении.

class Program 
{
 [STAThread]
 static void Main() 
 {
  ...

Объекты в пространстве имен System.Windows.Forms используют Win32 API функции, которые созданы для работы в одно-потоковом апартаменте. По этой причине программа Windows Forms должна иметь [STAThread] атрибут в функции main.

Из всего сказанного можно сделать следующий вывод. Windows Form и контролы Windows приложений используют WinApi функции. Все они создаются в рамках основного потока. В силу этого, нельзя вызывать метод или свойство контрола из другого потока нежели того, в котором он был создан. Все кросс-потоковые вызовы должны быть явно приведены к тому потоку, который создал форму или контрол (main поток). Для этого ранее использовались Control.Invoke или Control.BeginInvoke методы. Решением в .Net, обеспечивающим выполнение маршалинга между потоками, стало появление класса BackgroundWorker. Этот класс не только обеспечивает обертку для создаваемого потока - он также обеспечивая автоматический и скрытый от программиста вызов метода Control.Invoke и, таким образом, дает возможность контроля за ходом выполнения и завершения потока.


В начало

Параграф 2. Класс BackgroundWorker


В начало

2.1. BackgroundWorker - возможности и методы

BackgroundWorker - (System.ComponentModel.BackgroundWorker) класс, предназначенный для создания и управления работой потоков. Он предоставляет следующие возможности:

  • Стандартизированный протокол создания, сигнализации о ходе выполнения и завершения потока.

  • Возможность прерывания потока.

  • Возможность использования как контрола в Visual Studio Net.

  • Возможность обработки исключений в фоновом потоке.

  • Возможность связи с основным потоком через сигнализацию о ходе выполнения и окончания.

Таким образом, при использовании BackgroundWorker нет необходимости включения try/catch в рабочий поток и есть возможность выдачи информации в основной поток без явного вызова Control.Invoke.

BackgroundWorker предоставляет следующие основные методы:

  • RunWorkerAsync - служит для запуска потока.

  • DoWork - вызывается при старте потока. В методе можно определить код, который будет выполняться в отдельном потоке.

  • RunWorkerCompleted - вызывается по завершении выполнения потока.

  • ProgressChanged - может быть использован для отслеживания прогресса выполнения фонового потока.

  • CancelAsync - можно использовать для досрочного завершения фоновой операции. CancelAsync устанавливает соответствующее значение свойства CancellationPending. Фоновый код, поддерживающий досрочное завершение, проверяет значение этого свойства и, как только оно становится равным True, завершает свое выполнение.

Отметим, что ряд компонентов Windows Forms теперь поддерживает асинхронные операции. К таким компонентам, в частности, относятся компоненты SoundPlayer, PictureBox.

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


В начало

2.2. Использование контрола BackgroundWorker

Создадим простой проект решения (Рис.1.), поместив на форму пять контролов, четыре уже нам знакомых: Button, TextBox, Timer, ProgressBar и пятым, контрол со вкладки Components - BackgroundWorker.

winapp010.gif

Рис.1. Проект решения

Установим свойства для контролов:

  • TextBox:

    • MultiLine - True.

  • Timer:

    • Interval - 100.

    • Enabled - False.

  • ProgressBar:

    • Doc - Bottom.

    • Step - 1.

    • Maximum - 100.

  • BackgroundWorker:

    • Все по умолчанию.

Для контрола BackgroundWorker создадим обработчики событий DoWork и RunWorkerCompleted. Запуск фонового потока выполним в обработчике нажатия кнопки 1. Сам фоновый поток будет выполнять код функции functionThread2:

private void button1_Click(object sender, EventArgs e)
{
 //Методу  RunWorkerAsync можно передавать объект в качестве параметра
 backgroundWorker1.RunWorkerAsync();
}
private bool functionThread2()
{
 return true;
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
 e.Result = functionThread2();
}
private void backgroundWorker1_RunWorkerCompleted(object sender,RunWorkerCompletedEventArgs e)
{
 textBox1.Text = Convert.ToString(e.Result);
}

Результат выполнения кода показан на Рис.2.

winapp011.gif

Рис.2. Результат выполнения кода

Немного изменим код и убедимся, что контрол TextBox доступен только из основного потока и в методе RunWorkerCompleted (по завершении фонового потока), в то время как переменные приложения доступны в любом методе.

public partial class Form1 : Form
{
 //Объявим переменную
 private int viI = 0; 
 .....

 private void button1_Click(object sender, EventArgs e)
 {
  backgroundWorker1.RunWorkerAsync();
 }
 private bool functionThread2()
 {
  try
  {
   //Не вызывает прерывания
   viI++;
   //Вызывает прерывание
   //textBox1.Text = "In functionThread2 ";
   return true;
  }
  catch (Exception)
  {
   return false;
  }
 }
 private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
 {
  try
  {
   //Не вызывает прерывания
   viI++; 
   //Вызывает прерывание
   //textBox1.Text = "In backgroundWorker1_DoWork";
  }
  catch (Exception)
  { 
  }
  e.Result = functionThread2();
 }
 private void backgroundWorker1_RunWorkerCompleted(object sender,RunWorkerCompletedEventArgs e)
 {
  //Не вызывает прерывания
  viI++; 
  //Не вызывает прерывание
  textBox1.Text = Convert.ToString(e.Result) + Environment.NewLine;
  textBox1.Text += Convert.ToString(viI);
 }

Результат выполнения кода при любом не закомментированном обращении к textBox1 показан на Рис.3.

winapp012.gif

Рис.3. Результат выполнения кода

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

Установим свойство WorkerReportsProgress контрола BackgroundWorker в значение True и создадим обработчик события ProgressChanged для контрола BackgroundWorker и события Tick для Timer. Запишем следующий код:

public partial class Form1 : Form
{
 private int viI = 0; 
 public Form1()
 {
     InitializeComponent();
 }
 private void button1_Click(object sender, EventArgs e)
 {
  button1.Enabled = false;
  timer1.Enabled = true;
  progressBar1.Value = 0;
  backgroundWorker1.RunWorkerAsync();
 }
 private bool functionThread2()
 {
  for (int i = 0; i <= 100; i++)
  {
   backgroundWorker1.ReportProgress(i);
   Thread.Sleep (100);
  }        
 return true;            
 }
 private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
 {
  e.Result = functionThread2();           
 }
 private void backgroundWorker1_RunWorkerCompleted(object sender,RunWorkerCompletedEventArgs e)
 {
  textBox1.Text += "Completed";           
 }
 private void backgroundWorker1_ProgressChanged(object sender,ProgressChangedEventArgs e)
 {
  viI = e.ProgressPercentage;    
 } 
 private void timer1_Tick(object sender, EventArgs e)
 {
  if (viI != 0 && viI <= 100)
  {
   textBox1.Text += Convert.ToString(viI) + Environment.NewLine;
   progressBar1.Value = viI;
  }
  if (viI == 100)
  {
   timer1.Enabled = false;
   button1.Enabled = true;
   viI = 0;
  }
 }
}

Результат выполнения кода показан на Рис.4.

winapp013.gif

Рис.4. Результат выполнения кода


В начало

2.3. Программное создание BackgroundWorker

В некоторых случаях не является целесообразным держать в приложении контрол (объект контрола) на все время его работы (например, необходимо создать поток для инициализации приложения, которая занимает продолжительный период времени, а время инициализации использовать для отображения рекламной информации). В подобных случаях прибегают к программному созданию объектов. Создание объекта BackgroundWorker ничем не отличается от создания любых других объектов, которые представлены в Visual Studio .Net контролами. Для демонстрации используем проект решения параграфа 2.2. последний код, где мы использовали ProgressBar. Уберем из решения контрол BackgroundWorker, однако сохраним все функции примера. Добавим в обработчик нажатия кнопки 1 код:

public partial class Form1 : Form
{
 .....
 private BackgroundWorker backgroundWorker1=null;
 .....

 private void button1_Click(object sender, EventArgs e)
 {
  backgroundWorker1 = new BackgroundWorker();
  backgroundWorker1.WorkerReportsProgress = true;
  backgroundWorker1.WorkerSupportsCancellation = true;
  backgroundWorker1.DoWork += backgroundWorker1_DoWork;
  backgroundWorker1.ProgressChanged += backgroundWorker1_ProgressChanged;
  backgroundWorker1.RunWorkerCompleted += backgroundWorker1_RunWorkerCompleted;
  backgroundWorker1.RunWorkerAsync();
 }

Выполним приложение. Оно функционирует также, как и в предыдущем примере (Рис.4.).

При пропадании необходимости в объекте можно поставить код backgroundWorker1=null.


В начало

Параграф 3. Синхронизация потоков, созданных на основе класса BackgroundWorker

В Windows реализована вытесняющая многозадачность, и, как отмечено выше, возможны ситуации, такие как, например, когда при параллельном вызове одного и того же метода объекта, равно как и обращении к статической или глобальной переменной, один поток модифицировал объект (переменную) - другой, получив квант времени, восстановил свои значения. Эта проблема возникает достаточно часто, когда два или более потока используют какой-либо общий ресурс. Для исключения подобных ситуаций в дополнение к механизмам Apartmens используются метода синхронизации потоков. О некоторых из них, применительно к возможности использования с объектами класса BackgroundWorker, будет идти речь в данном параграфе.


В начало

3.1. Использование класса ReaderWriterLock

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

Класс ReaderWriterLock используется для синхронизации именно такой потоковой модели (один "писатель" - много "читателей"). В любое время он позволяет или одновременный доступ для чтения для нескольких потоков, или доступ для записи только для одного потока. В тех ситуациях, когда ресурс изменяется не часто, класс ReaderWriterLock предоставляет лучшую пропускную способность, чем другие модели.

Класс ReaderWriterLock содержит раздельные метода для чтения и записи - AcquireReaderLock и AcquireWriterLock.

Так как искажения данных могут возникнуть только при записи, то в данной модели, поток, желающий записать информацию в охраняемый объект, сначала должна получить разрешение на запись у объекта AcquireWriterLock класса ReaderWriterLock. Если разрешение не получено, то поток приостанавливается до освобождения ресурса или до истечения срока заданного таймаута. Аналогично получаются разрешение на чтение (AcquireReaderLock), чтобы не мешать потоку записи. Блокировки освобождаются при вызове ReleaseReaderLock или ReleaseWriterLock.

Для демонстрации механизма решим такую задачу: Пусть два потока регулярно пишут в память результаты продажи рогов и копыт (первый рогов, второй копыт) некоторой фирмой. Множество нетерпеливых Остапов и Паниковских ждет эти данные. Желательным является, чтобы запись происходила последовательно (цикл записи рога/копыта, далее возможность чтения). На период записи данных их выдача должна блокироваться, в остальное время - Остапы и Паниковские свободно пользуются материалом.

Рассмотрим код, в котором реализован процесс получения разрешения на период записи (первая часть поставленной задачи).

public partial class Form1 : Form
{
 //Переменные для хранения результата
 private int viHorns = 0;
 private int viHoovs = 0;
 private int viSum = 0;
 //Объявление  ReaderWriterLock
 static ReaderWriterLock readerWriterLock = new ReaderWriterLock();   
 //Потоки для записи
 private BackgroundWorker backgroundWorker1, backgroundWorker2 = null;
 public Form1()
 {
  InitializeComponent();
 }
 //Создание объектов потоков и замыкание их на одни и теже обработчики
 private void createThreadWrite(object sender)
 {
  textBox1.Text = "";
  ((BackgroundWorker)sender).WorkerReportsProgress = true;            
  ((BackgroundWorker)sender).WorkerSupportsCancellation = true;
  ((BackgroundWorker)sender).DoWork += backgroundWorker1_DoWork;
  ((BackgroundWorker)sender).ProgressChanged += backgroundWorker1_ProgressChanged;
  ((BackgroundWorker)sender).RunWorkerCompleted += backgroundWorker1_RunWorkerCompleted;
  ((BackgroundWorker)sender).RunWorkerAsync();
 }
 //Инициализация записи по нажатию кнопки
 private void button1_Click(object sender, EventArgs e)
 {
  backgroundWorker1 = new BackgroundWorker();
  createThreadWrite(backgroundWorker1);
  backgroundWorker2 = new BackgroundWorker();
  createThreadWrite(backgroundWorker2);
 }
 //Функция потоков записи
 private void functionThreadWrite(int viN)
 {
  //Получение разрешения на запись с таймаутом ожидания
  readerWriterLock.AcquireWriterLock(3000);
  if (viN == 1)
  {
   viHorns = new Random().Next(0,3); 
  }
  else
  {
   viHoovs = new Random().Next(0,5);
  }
  //Имитироум, что поток длительный
  Thread.Sleep (2000);
  //Снятие блокировки
  readerWriterLock.ReleaseWriterLock();
 }
 //Старт фоновых функций потоков
 private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
 {
  if (sender == (object)backgroundWorker1)
  {
   functionThreadWrite(1);
  }
  else
  {
   functionThreadWrite(2);
  }
 }
 //Завершение потоков
 private void backgroundWorker1_RunWorkerCompleted(object sender,RunWorkerCompletedEventArgs e)
 {
  if (sender == (object)backgroundWorker1)
  {
      textBox1.Text += "Completed 1" + Environment.NewLine;
      textBox1.Text += "Hoorns " + Convert.ToString(viHorns) + Environment.NewLine;
      viSum += viHorns;
  }
  else
  {
      textBox1.Text += "Completed 2" + Environment.NewLine;
      textBox1.Text += "Hoovs " + Convert.ToString(viHoovs) + Environment.NewLine;
      viSum += viHoovs;
  }
  textBox1.Text += "Amount: " + Convert.ToString(viSum) + Environment.NewLine; 
 }      
}

Результаты выполнения кода показаны на Рис.5. Какой поток завершится первым без использования получения разрешения на запись (в случае когда readerWriterLock.AcquireWriterLock(3000) и readerWriterLock.ReleaseWriterLock() закомментированы) угадать не возможно. Видно, что на рисунке слева последовательность завершения потоков не та, что мы ожидали. Тот поток, который был запущен первым выполнился вторым, что, как мы указали в постановке задачи, для нас нежелательно. На рисунке справа второй поток "добросовестно" подождал 2000 с небольшим миллисекунд и выполнил свою задачу.

winapp014.gif

Рис.5. Результат выполнения кода

Рассмотрим код, который полностью реализует поставленную задачу. В коде по кнопке 1 блокируется таймер, что бы было возможно остановить все потоки. Формирование классов потоков вынесено в конструктор формы. Создаются два потока для записи и два для чтения. Все потоки не мешают друг другу - потоки, которые читают из переменной viSum и потоки записи не прерывают друг друга, что видно из Рис.6. В коде добавлена синхронизация потоков по таймеру, что бы запись шла одним блоком.

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

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading; 
namespace WA001
{
 public partial class Form1 : Form
 {
  //Защищаемая переменная  Рога+Копыта
  private int viSum = 0;
  //Рога
  private int viHorns = 0;
  //Копыта
  private int viHoovs = 0;
  //Вспомогательные переменные
  private int viI = 0,viJ=0,viK=0;
  //Переменные для чтения
  private int viSumRead3 = 0;
  private int viSumRead4 = 0;
  //Переменные сигнализации для запуска потоков
  private bool fBl1 = false;
  private bool fBl2 = false;
  private bool fBl3 = false;
  private bool fBl4 = false; 
  //Создаем объект для синхронизации
  static ReaderWriterLock readerWriterLock = new ReaderWriterLock();
  //Объекты для записи
  private BackgroundWorker backgroundWorker1, backgroundWorker2 = null;
  //Объекты для чтения
  private BackgroundWorker backgroundWorker3, backgroundWorker4 = null;
  public Form1()
  {
   InitializeComponent();
   backgroundWorker1 = new BackgroundWorker();
   backgroundWorker1.DoWork += backgroundWorker1_DoWork;
   backgroundWorker1.RunWorkerCompleted += backgroundWorker1_RunWorkerCompleted;
   backgroundWorker2 = new BackgroundWorker();
   backgroundWorker2.DoWork += backgroundWorker1_DoWork;
   backgroundWorker2.RunWorkerCompleted += backgroundWorker1_RunWorkerCompleted;
   backgroundWorker3 = new BackgroundWorker();
   backgroundWorker3.DoWork += backgroundWorker3_DoWork;
   backgroundWorker3.RunWorkerCompleted += backgroundWorker3_RunWorkerCompleted;
   backgroundWorker4 = new BackgroundWorker();
   backgroundWorker4.DoWork += backgroundWorker3_DoWork;
   backgroundWorker4.RunWorkerCompleted += backgroundWorker3_RunWorkerCompleted;
   timer1.Enabled = true;
  }
  //Функция потоков для выполнения записи
  private void functionThreadWrite(int viWhere)
  {
   try
   {
    //Запрос для получения разрешения на запись
    readerWriterLock.AcquireWriterLock(Timeout.Infinite);
    //Имитируем, что поток длительный
    Thread.Sleep(600);
    switch (viWhere)
    {
     case 1:
      viHorns = new Random().Next(0, 3);
      viSum += viHorns;
     break;
     case 2:
      viHoovs = new Random().Next(0, 5);
      viSum += viHoovs;
     break;
    }
   }
   finally
   {
    readerWriterLock.ReleaseWriterLock();
   }
  }
  //Функция потоков для выполнения чтения
  private void functionThreadRead(int viWhere)
  {
   try
   {
    //Получение разрешения на чтение
    readerWriterLock.AcquireReaderLock(Timeout.Infinite);
    //Имитируем, что поток длительный
    Thread.Sleep(500);
    switch (viWhere)
    {
     case 3:
      viSumRead3 = viSum;
     break;
     case 4:
      viSumRead4 = viSum;
     break;
    }                
   }
   finally
   {
    readerWriterLock.ReleaseReaderLock();
   }
  }
  //Реакция на событие DoWork при записи
  private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
  {
   if (sender == (object)backgroundWorker1)
   {
    functionThreadWrite(1);
   }
   else
   {
   functionThreadWrite(2);
   }        
  }
  //Реакция на событие DoWork при чтении
  private void backgroundWorker3_DoWork(object sender, DoWorkEventArgs e)
  {
   if(sender == (object)backgroundWorker3)
   {
    functionThreadRead(3);
   }
   else
   {
    functionThreadRead(4);
   }
  }
  //Реакция на событие RunWorkerCompleted при записи
  private void backgroundWorker1_RunWorkerCompleted(object sender,RunWorkerCompletedEventArgs e)
  {
   if (sender == (object)backgroundWorker1)
   {
    textBox1.Text += "Completed 1" + Environment.NewLine;
    fBl1 = false;
    viJ = 0;
   }
   else
   {
    textBox1.Text += "Completed 2" + Environment.NewLine;
    fBl2 = false;
    viK = 0;
   }
   textBox1.Text += "Hoorns: " + Convert.ToString(viHorns) +
      " Hoovs " + Convert.ToString(viHoovs) + Environment.NewLine;
   textBox1.Text += "Amount: " + Convert.ToString(viSum) + Environment.NewLine;
  }
  //Реакция на событие RunWorkerCompleted при чтении
  private void backgroundWorker3_RunWorkerCompleted(object sender,RunWorkerCompletedEventArgs e)
  {
   if (sender == (object)backgroundWorker3)
   {
    textBox1.Text += "Completed 3" + Environment.NewLine;
    fBl3 = false;
   }
   else
   {
    textBox1.Text += "Completed 4" + Environment.NewLine;
    fBl4 = false;
   }
   //textBox1.Text += "Hoorns: " + Convert.ToString(viHorns) +
   //      " Hoovs " + Convert.ToString(viHoovs) + Environment.NewLine;
   textBox1.Text += "Amount: " + Convert.ToString(viSum) + Environment.NewLine;
  }
  //Асинхронный запуск потоков с использованием таймера
  private void timer1_Tick(object sender, EventArgs e)
  {
   //viI % 20 имитирует то, что запись выполняется редко
   if (!fBl1 && !fBl2 && (viI % 20) == 0)
   {
    fBl1 = true;
    fBl2 = true;
    viJ = 1;
    backgroundWorker1.RunWorkerAsync();
    viK = 1;
    backgroundWorker2.RunWorkerAsync();
   }
   viI++;
   //Можно дополнительно заблокировать чтение на период записи.
   //В этом случае между двумя потоками записи не 
   //будут появляться потоки чтения.
   if (viJ != 0 || viK != 0) return;
   if (!fBl3)
   {
    fBl3 = true;
    backgroundWorker3.RunWorkerAsync();
   }
   if (!fBl4)
   {
    fBl4 = true;
    backgroundWorker4.RunWorkerAsync();
   }
  }
  //Старт/стоп
  private void button1_Click(object sender, EventArgs e)
  {
   timer1.Enabled= !timer1.Enabled;
  }     
 }
}

winapp015.gif

Рис.6. Результат выполнения кода


В начало

3.2. Синхронизация с использованием класса Monitor

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

Monitor.Enter(my_object);
......
Monitor.Exit(my_object);

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

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

Для демонстрации работы монитора воспользуемся кодом предыдущего примера. Внесем небольшие изменения:

  • Добавим объект защиты:

    public partial class Form1 : Form
    {
     .....
    

    private static object my_object = new object();

     .....
    
  • Методы обработки событий RunWorkerCompleted и DoWork для чтения и записи оставим без изменения.

  • В функциях потоков functionThreadWrite и functionThreadRead:

    • Установим приблизительную (время когда поток спит и время выполнения нескольких строк кода) длительность выполнения для потоков functionThreadRead - Thread.Sleep(400); а для functionThreadWrite - Thread.Sleep(1000); Смысл - потоки записи выполняются дольше потоков чтения.

    • Уберем методы AcquireReaderLock, ReleaseReaderLock, AcquireWriterLock, ReleaseWriterLock.

    • Вместо удаленных методов получения разрешений запишем методы защиты объекта my_object:

      functionThreadWrite примет вид:

      private void functionThreadWrite(int viWhere)
      {
       try
       {
      

      Monitor.Enter(my_object);            //**** Строка 1

        //Имитируем, что поток длительный
        Thread.Sleep(1000);                    
        switch (viWhere)
        {
         case 1:
          viHorns = new Random().Next(0, 3);
          viSum += viHorns;
         break;
         case 2:
          viHoovs = new Random().Next(0, 5);
          viSum += viHoovs;
         break;
        }
       }
       finally
       {
      

      Monitor.Exit(my_object);            //**** Строка 2

       }
      }
      private void functionThreadRead(int viWhere)
      {
       try
       {
      

      Monitor.Enter(my_object);            //**** Строка 3

         //Имитируем, что поток длительный
         Thread.Sleep(200);
         switch (viWhere)
         {
          case 3:
           viSumRead3 = viSum;
           break;
          case 4:
           viSumRead4 = viSum;
           break;
         }                
        }
        finally
        {
      

      Monitor.Exit(my_object);            //**** Строка 4

        }
      }
      
  • И последнее, в метод обработки прерываний таймера добавим одну строку:

     
    private void timer1_Tick(object sender, EventArgs e)
    {
     if (!fBl1 && !fBl2 && (viI % 20) == 0)
     {
      fBl1 = true;
      fBl2 = true;
      viJ = 1;
      backgroundWorker1.RunWorkerAsync();
      viK = 1;
      backgroundWorker2.RunWorkerAsync();
     }
     viI++;
    

    if (viJ != 0 || viK != 0) return;            //**** Строка 5

     if (!fBl3)
     {
      fBl3 = true;
      backgroundWorker3.RunWorkerAsync();
     }
     if (!fBl4)
     {
      fBl4 = true;
      backgroundWorker4.RunWorkerAsync();
     }            
    }
    

Напомним, что таймер пытается запустить потоки каждые 100мс. Таким образом, потоки будут вновь запускаться практически сразу после своего окончания. Результат работы кода показан на Рис.7.

winapp016.gif

Рис.7. Результат выполнения кода

Испытание кода проведено для четырех случаев, путем добавления и удаления помеченных как "//****" строк кода.

  • Рисунок слева - случай когда все 5 помеченных строк закомментированы (синхронизация отсутствует). Нетрудно заметить, что потоки периодически вытесняют друг друга и какой из них закончится ранее другого предугадать нельзя.

  • Второй слева рисунок соответствует случаю, когда установлена защита для объекта в функции записи (не закомментированы "Строка 1" и "Строка 2"). Потоки 1 и 2 никто не вытесняет и по окончании каждого из них ОС успевает предоставить кванты времени потокам 3 и 4 для их неоднократного исполнения.

  • Третий слева рисунок соответствует случаю, когда установлена защита для объекта в функциях записи и чтения (не закомментированы Строка 1, 2, 3 и 4). В данном случае никто из потоков не прерывает друг друга, однако по окончании очередного потока сохраняется борьба за квант времени.

  • Рисунок справа показывает случай (не закомментированы все помеченные строки), когда добавляется и синхронизация по таймеру. Поставленная задача предыдущего параграфа можно сказать выполнена, но, все же, при отсутствии вытеснений одного потока другим, конкуренция между потоками в группе записи (между потоками 1 и 2) и группе чтения (потоками 3 и 4) сохраняется. Для того, чтобы избежать этого можно использовать дополнительные блокировки (например по таймеру).

В заключении параграфа отметим, что в C# возможно неявное использование мониторов с помощью ключевого слова lock (использование lock предпочтительно из за дополнительной синтаксической проверки типа значения).

lock (my_object) 
{ 
 ... 
}
//эквивалентно
try 
{ 
 Monitor.Enter( my_object ); 
... 
}  
finally 
{ 
 Mointor.Exit( my_object ); 
}


В начало

3.3. Использование класса EventWaitHandle

Усложним целевую задачу параграфа 3.1. Потребуем, чтобы поток 1 писал всегда перед потоком 2 (данные о рогах должны писаться всегда ранее данных о копытах). В 3.1. и 3.2. мы добились, того, что потоки 1 и 2 выполняются в блоке, но кто первый - дело случая.

Ясно, что для решения такого типа задач необходимо, чтобы потоки сигнализировали друг другу о своем завершении (в нашем случае, поток 2 должен ждать какого либо сигнала от потока 1). Сигнализацию между потоками и призван обеспечить класс EventWaitHandle.

Класс EventWaitHandle позволяет приостановить поток до получения сигнала от другого потока или до окончания таймаута и подать сигнал на окончание ожидания. Для этого у класса есть методы WaitOne, WaitAll, WeitAny (метуды класса родителя WaitHandle) и метод Set, другие методы нас пока не интересуют.

Отметим, что метод WaitOne принимает необязательный параметр таймаута и возвращает true - если ожидание закончилось по сигналу и false - по таймауту. Кроме того, метод Reset позволяет прекратить ожидание без всяких условий в любом месте выполняемого кода потока.

Для демонстрации использования методов класса воспользуемся кодом параграфа 3.2, который у нас получился после модернизации кода из параграфа 3.1. (можно использовать и код параграфа 3.1.) Иначе - мы будем использовать совместно Monitor и EventWaitHandle. Добавим всего три строчки кода :

  • Добавим объект класса EventWaitHandle:

    public partial class Form1 : Form
    {
     .....
    

    private static EventWaitHandle eventWaitHandle = new AutoResetEvent(false);
         //Или так
         //EventWaitHandle eventWaitHandle = new EventWaitHandle(false, EventResetMode.Auto);
         //Или так
         //EventWaitHandle eventWaitHandle = new EventWaitHandle(false, EventResetMode.Auto, "Имя");

         //для освобождения ресурсов операционной системы нужно вызывать метод Close, когда объект будет не нужен

     .....
    

    Обратим внимание на некоторые моменты:

    • В приведенном выше коде мы создаем объект класса EventWaitHandle на основе его потомка - класса AutoResetEvent (можно ManualResetEvent - оба класса включают функциональные возможности базового класса, а отличие заключается в том, что вызываются конструктор базового класса с различными параметрам).

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

  • В функции functionThreadWrite в операторе case 2: добавим перевод потока в режим ожидания:

    case 2:
    

    if (fBl1 && !fBl2) eventWaitHandle.WaitOne();

     ... 
    break;
    
  • В методе обработки события RunWorkerCompleted в backgroundWorker1_RunWorkerCompleted для потока 1 добавим строку, сброса режима ожидания:

    if (sender == (object)backgroundWorker1)
    {
     ......
     fBl1 = false;
    

    eventWaitHandle.Set();

    }
    

Смысл добавленных строк - поток 2, если выполняется поток 1, переходит в режим ожидания, пока его не выведет из этого режима поток 1.

Запустим код и убедимся, что поток 2 всегда следует после потока 1.

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


В начало

3.4. Использование классов Mutex и Semaphore

Классы Mutex и Semaphore, как и класс EventWaitHandle, являются наследниками абстрактного класса WaitHandle. Таким образом, они наследуют некоторые общие методы для всех трех классов. Отличие заключается в том, что, если класс EventWaitHandle позволяет приостановить поток до получения сигнала от другого потокока или до окончания таймаута, то, при использовании Mutex, поток, получивший семафор, приостанавливает доступ к ресурсу, пока сам не освободит этот ресурс (вернет семафор). Кроме того Mutex блокирует ресурс на уровне компьютера, а не приложения. Задание имени и установка Mutex блокировки в функции main часто используется для предотвращения запуска второго экземпляра приложения, как показано в приведенном ниже коде:


using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Threading;

namespace WA001
{
 static class Program
 {
  static Mutex mutex = new Mutex(false, "My Name Start");
  [STAThread]
  static void Main()
  {
   //Ожидаем получения мьютекса 2 сек - если уже есть запущенный
   //экземпляр с именем My Name Start, то новое приложение 
   //завершается.
   if(!mutex.WaitOne(TimeSpan.FromSeconds(2), false)) 
   {
    return;
   }
   Application.EnableVisualStyles();
   Application.SetCompatibleTextRenderingDefault(false);
   Application.Run(new Form1());
  }
 }
}

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

....
private static Mutex mutex = new Mutex(false, "My Name")
....
private void functionThreadWrite(int viWhere)
{
 try
 { 
  mutex.WaitOne(TimeSpan.FromSeconds(2) ,true);
  Thread.Sleep(1000);
  switch (viWhere)
  {
   case 1:
    viHorns = new Random().Next(0, 3);
    viSum += viHorns;
   break;
   case 2:
     viHoovs = new Random().Next(0, 5);
     viSum += viHoovs;
     break;
   }
 }
 finally
 {
   mutex.ReleaseMutex();      
 }
}

Приведенный код не позволяет выполнить взаимное вытеснение друг друга потоков 1 и 2.

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

static Semaphore semaphore = new Semaphore(2,2, "My Name1");
private void functionThreadWrite(int viWhere)
{
 try
 { 
  semaphore.WaitOne(TimeSpan.FromSeconds(2) ,true);
  Thread.Sleep(1000);
  switch (viWhere)
  {
   case 1:
    viHorns = new Random().Next(0, 3);
    viSum += viHorns;
   break;
   case 2:
     viHoovs = new Random().Next(0, 5);
     viSum += viHoovs;
     break;
   }
 }
 finally
 {
   semaphore.ReleaseMutex();      
 }
}

Как и класс Mutex класс Semaphore можно использовать для блокировки запуска не более 1, 2, 3 и т.д. экземпляров приложений.


using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Threading;

namespace WA001
{
 static class Program
 {
  //Запущено будет не более 2х приложений
  static Semaphore semaphore1 = new Semaphore(2, 2, "My Name Start");
  [STAThread]
  static void Main()
  {
   if (!semaphore1.WaitOne(TimeSpan.FromSeconds(2), false))
                return;
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}


В начало

3.5. Использование таймеров

В .NET представлено три вида таймеров:

  • В пространстве имен System.Windows.Form. Его мы использовали как вспомогательное средство синхронизации потоков. Данный таймер не создает самостоятельного потока. Его задача запускать периодически выполняемые операции в рамках главного потока.

  • В пространстве имен System.Threading. Данный таймер создает поток для некоторой процедуры, вызываемой через заданный интервал времени.

  • В пространстве имен System.Timers. Класс таймера не является опечатанным и может использоваться для создания собственных классов-потомков с собственными обработчики событий, каждый из которых способен работать с отдельным потоком.

Далее мы рассмотрим использование таймеров для решения задачи, постановка которой описана в п.3.1. Код реализации претерпит сильные изменения. Поэтому рекомендуется убрать ранее сформированный код, оставив пустотелые обработчики событий контролов и вновь приступить к формированию целевого кода.


В начало

3.5.1. Использование таймера пространства имен System.Threading

Пусть по кнопке 1 запускается цикл записи о продажах рогов и копыт. Будем, как и ранее, считать, что потоки записи длительные и должны выполняться в фоне. Пусть интервал между циклами записи будет равен 5 секунд.

Решение этой задачи дает следующий код, пояснения к которому даны в тексте и в замечаниях, приведенных ниже:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;
namespace WA001
{
 public partial class Form1 : Form
 {
  private int viHorns = 0;
  private int viHoovs = 0;
  private int viSum = 0;
  private bool fBl1 = true;
  private bool fBl2 = true;
  public Form1()
  {
   InitializeComponent();                 
   timer1.Enabled = true;
  }
  private void button1_Click(object sender, EventArgs e)
  {
   //Создаем два таймера, которые вызывают в фоне каждые 
   //пять секунд одну и туже процедуру
   System.Threading.Timer
    timer1 = new System.Threading.Timer(functionThreadWrite,"1",0,5000);
   System.Threading.Timer        
    timer2 = new System.Threading.Timer(functionThreadWrite,"2",0,5000);
  }
  //Функция фонового потока
  public void functionThreadWrite(object data)
  {
   int viWhere = Convert.ToInt32(data);   
   Thread.Sleep(7000);
   switch (viWhere)
   {
    case 1:
     viHorns = new Random().Next(0, 3);
     viSum += viHorns;
     break;
    case 2:
     viHoovs = new Random().Next(0, 5);
     viSum += viHoovs;
    break;
   }
   if(fBl1) fBl1 = false;
   if(fBl2) fBl2 = false;           
  }
  //Используем таймер для вывода
  private void timer1_Tick(object sender, EventArgs e)
  {
   if (!fBl1)
   {
    fBl1 = true;
    vOutput(1);
   }
   if (!fBl2)
   {
    fBl2 = true;
    vOutput(2);
   }
 }
 private void vOutput(int i)
 {
   switch(i)
   {
    case 1:
     textBox1.Text += "Completed 1" + Environment.NewLine;
     textBox1.Text += "Hoorns: " + Convert.ToString(viHorns) +
        " Hoovs " + Convert.ToString(viHoovs) + Environment.NewLine;
    break;
    case 2:
     textBox1.Text += "Completed 2" + Environment.NewLine;
     textBox1.Text += "Hoorns: " + Convert.ToString(viHorns) +
       " Hoovs " + Convert.ToString(viHoovs) + Environment.NewLine;
    break;
   }
   textBox1.Text += "Amount: " + Convert.ToString(viSum) + Environment.NewLine;
  }
}

Замечания по коду решения:

  • Как и ранее, объекты контролов формы недоступны в фоновых потоках, поэтому вывод в textBox пришлось выполнить в отдельной функции, запускаемой по времени.

  • При выполнении кода можно заметить, что первый вывод результатов происходит через 7 секунд, причем выводится и Completed 1 и Completed 2 практически одновременно, далее результаты выводятся через 5 секунд. Это говорит о том, что по каждому вызову таймера создается новый поток и потоки не пересекаются.

Приведенный код показывает, что сам таймер может создавать потоки. Но, используя данный таймер, можно выполнить запуск не только одиночной функции в фоновом потоке, но и поток, созданный при помощи класса BackgroundWorker, что и демонстрирует код, приведенный ниже.

Внесем некоторые изменения в коде предыдущего примера, а именно:

//Добавим определение объектов класса BackgroundWorker и ряд переменных,
//которые будем использовать для синхронизации:
private BackgroundWorker backgroundWorker3, backgroundWorker4 = null;
private bool fBl3 = false;
private bool fBl4 = false;
private bool fBl3_1 = false;
private bool fBl4_1 = false;
private static object my_object = new object();
....
//При инициализации создадим объекты backgroundWorker3 и backgroundWorker4: 
public Form1()
{
 InitializeComponent();                 
 timer1.Enabled = true;
 backgroundWorker3 = new BackgroundWorker();
 backgroundWorker3.DoWork += backgroundWorker3_DoWork;
 backgroundWorker3.RunWorkerCompleted += backgroundWorker3_RunWorkerCompleted;
 backgroundWorker4 = new BackgroundWorker();
 backgroundWorker4.DoWork += backgroundWorker3_DoWork;
 backgroundWorker4.RunWorkerCompleted += backgroundWorker3_RunWorkerCompleted;
}
//Добавим еще два таймера
private void button1_Click(object sender, EventArgs e)
{
 .........
 System.Threading.Timer
  timer3 = new System.Threading.Timer(timerTick3,"3",0,100);
 System.Threading.Timer        
  timer3 = new System.Threading.Timer(timerTick3,"4",0,100);
}
//Добавим функцию выполнения потока таймера, в которой будут 
//запускаться другие потоки и осуществляться блокировка
//от повторных запусков:
public void timerTick3(object data)
{
 int viWhere = Convert.ToInt32(data);
 switch (viWhere)
 {
  case 3:
   if (fBl3_1) return;
  break;
  case 4:
   if (fBl4_1) return;
  break;
 }
 try
 {
  Monitor.Enter(my_object); 
  switch (viWhere)
  {
   case 3:
    fBl3_1 = true;
    backgroundWorker3.RunWorkerAsync();
   break;
   case 4:
    fBl4_1 = true;
    backgroundWorker4.RunWorkerAsync();
   break;
  }
 }
 finally
 {
  Monitor.Exit(my_object); 
 }
}
//Метод  DoWork для потоков backgroundWorker3 и backgroundWorker4
//ассоциируем с functionThreadRead
private void backgroundWorker3_DoWork(object sender, DoWorkEventArgs e)
{
 if (sender == (object)backgroundWorker3)
 {
  functionThreadRead(3);
 }
 else
 {
  functionThreadRead(4);
 }
}
//Функцию потоков запишим как:
private void functionThreadRead(int viWhere)
{
 //Имитируем, что поток длительный
 Thread.Sleep(400);
 switch (viWhere)
 {
  case 3:
   viSumRead3 = viSum;
  break;
  case 4:
   viSumRead4 = viSum;
  break;
 }
}
//В функцию vOutput добавим:
case 3:
 textBox1.Text += "Completed 3" + Environment.NewLine;
break;
case 4:
 textBox1.Text += "Completed 4" + Environment.NewLine;
break;
//Метод backgroundWorker3_RunWorkerCompleted запишем как:
private void backgroundWorker3_RunWorkerCompleted(object sender, 
                                      RunWorkerCompletedEventArgs e)
{
 if (sender == (object)backgroundWorker3)
 {
  fBl3 = false;
  fBl3_1 = false;
 }
 else
 {
  fBl4 = false;
  fBl4_1 = false;
 }
}
//Синхронизацию вывода оформим в timer1_Tick:
private void timer1_Tick(object sender, EventArgs e)
{
 if (!fBl1)
 {
  fBl1 = true;
  vOutput(1);
 }
 if (!fBl2)
 {
  fBl2 = true;
  vOutput(2);
 }
 if (!fBl3)
 {
  fBl3 = true;
  vOutput(3);
 }
 if (!fBl4)
 {
  fBl4 = true;
  vOutput(4);
 }
 return;
}

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


В начало

3.5.2. Использование таймера пространства имен System.Timers

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

В приведенном ниже коде, в отличие от предыдущего примера, использован класс таймера пространства имен System.Timers для запуска по условиям потоков 3 и 4. Код приводится полностью, пояснения к коду приведены ниже:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;
namespace WA001
{
 public partial class Form1 : Form
 {
  private int viHorns = 0;
  private int viHoovs = 0;
  private int viSum = 0;
  private int viSumRead3 = 0;
  private int viSumRead4 = 0;
  private bool fBl1 = true;
  private bool fBl2 = true;
  private bool fBl3 = false;
  private bool fBl4 = false;

static private bool fBl3_1 = false;
     static private bool fBl4_1 = false;
     static private BackgroundWorker backgroundWorker3, backgroundWorker4 = null;
     public myTimer mytimer;

  private static object my_object = new object();
  public Form1()
  {
   InitializeComponent();
   backgroundWorker3 = new BackgroundWorker();
   //backgroundWorker3.DoWork += backgroundWorker3_DoWork;
   backgroundWorker3.RunWorkerCompleted += backgroundWorker3_RunWorkerCompleted;
   backgroundWorker4 = new BackgroundWorker();
   //backgroundWorker4.DoWork += backgroundWorker3_DoWork;
   backgroundWorker4.RunWorkerCompleted += backgroundWorker3_RunWorkerCompleted;
   timer1.Enabled = true;
  }
  public void functionThreadWrite(object data)
  {
   int viWhere = Convert.ToInt32(data);
   Thread.Sleep(7000);
   switch (viWhere)
   {
    case 1:
     viHorns = new Random().Next(0, 3);
     viSum += viHorns;
    break;
    case 2:
     viHoovs = new Random().Next(0, 5);
     viSum += viHoovs;
    break;
   }
   if (fBl1) fBl1 = false;
   if (fBl2) fBl2 = false;
  }

public class myTimer : System.Timers.Timer
     {
      private int viStart, viStop;
      public bool fStart = false;
      public int countStart { get { return viStart; } }
      public int countStop { get { return viStart + viStop; } }
      public int countTemp = 0;
      public myTimer(int start, int stop)
      {
       viStart = start;
       viStop = stop;
       countTemp = 0;
       AutoReset = true;
       Interval = 100;
       fStart = false;
       Elapsed += new System.Timers.ElapsedEventHandler(timer_Tick);
      }
     }

    static void timer_Tick(object sender, System.Timers.ElapsedEventArgs e)
     {
     myTimer mytimer = (myTimer)sender;
     mytimer.countTemp = mytimer.countTemp+(int)mytimer.Interval;
     if (mytimer.countTemp > mytimer.countStart && mytimer.fStart == false)
     {
      mytimer.fStart = true;
     }
     if (mytimer.countTemp < mytimer.countStop && mytimer.fStart == true)
     {
      try
      {
       Monitor.Enter(my_object);
       if (mytimer.countTemp % 1000 == 0)
       {
        if (!fBl3_1)
        {
         fBl3_1 = true;
         backgroundWorker3.RunWorkerAsync();
        }
       }
       else
       {
        if (!fBl4_1)
        {
         fBl4_1 = true;
         backgroundWorker4.RunWorkerAsync();
        }
       }
      }
      finally
      {
       Monitor.Exit(my_object);
      }
     }
    }
    if (mytimer.countTemp > mytimer.countStop)
    {
     mytimer.Stop();
     mytimer.Close();
    }
   }

            
  private void button1_Click(object sender, EventArgs e)
  {

mytimer = new myTimer(200, 10000);
      mytimer.Start();

  System.Threading.Timer
  timer1 =
   new System.Threading.Timer(functionThreadWrite, "1", 0, 1000);
  System.Threading.Timer
   timer2 =
    new System.Threading.Timer(functionThreadWrite, "2", 0, 1000);
   }
   private void functionThreadRead(int viWhere)
   {
    try
    {
     //Имитируем, что поток длительный
     Thread.Sleep(400);
     switch (viWhere)
     {
      case 3:
       viSumRead3 = viSum;
      break;
      case 4:
       viSumRead4 = viSum;
      break;
     }
    }
    finally
    {
    }
   }
   private void vOutput(int i)
   {
    switch (i)
    {
     case 1:
      textBox1.Text += "Completed 1" + Environment.NewLine;
      textBox1.Text += "Hoorns: " + Convert.ToString(viHorns) +
            " Hoovs " + Convert.ToString(viHoovs) + Environment.NewLine;
     break;
     case 2:
      textBox1.Text += "Completed 2" + Environment.NewLine;
      textBox1.Text += "Hoorns: " + Convert.ToString(viHorns) +
             " Hoovs " + Convert.ToString(viHoovs) + Environment.NewLine;
     break;
     case 3:
      textBox1.Text += "Completed 3" + Environment.NewLine;
     break;
     case 4:
      textBox1.Text += "Completed 4" + Environment.NewLine;
     break;
    }
    textBox1.Text += "Amount: " + Convert.ToString(viSum) + Environment.NewLine;
   }
   private void backgroundWorker3_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
   {
    if (sender == (object)backgroundWorker3)
    {
     fBl3 = false;
     fBl3_1 = false;
    }
    else
    {
     fBl4 = false;
     fBl4_1 = false;
    }
   }
   private void timer1_Tick(object sender, EventArgs e)
   {
   if (!fBl1)
   {
    fBl1 = true;
    vOutput(1);
   }
   if (!fBl2)
   {
    fBl2 = true;
    vOutput(2);
   }
   if (!fBl3)
   {
    fBl3 = true;
    vOutput(3);
   }
   if (!fBl4)
   {
    fBl4 = true;
    vOutput(4);
   }
   return;
  }
 }
}

Особенности реализации кода (выделено зеленым цветом):

  • Ряд переменных определено как статические (в том числе и объекты классов BackgroundWorker). Это необходимо для обеспечения доступа к ним из функций потоков класса таймера.

  • Класс таймера myTimer объявлен объектом класса System.Timers.Timer и в нем переопределен конструктор и введены ряд дополнительных переменных. Событие Elapsed замкнуто на функцию timer_Tick, которая выполняется в потоке, созданном при запуске таймера.

    Elapsed += new System.Timers.ElapsedEventHandler(timer_Tick);
    
  • В функции timer_Tick выполняется запуск потоков 3 и 4 с учетом времени старта и окончания их выполнения. Момент старта и окончания передаются в конструкторе объекта класса myTimer.

  • В обработчике нажатия кнопки 1 выполняется создание и старт таймера:

    mytimer = new myTimer(200, 10000);
    mytimer.Start(); 
    


В начало

Литература:


В начало

Молчанов Владислав 27.09.2007г.

Еcли Вы пришли с поискового сервера - посетите мою главную страничку

На главной странице Вы найдете программы комплекса Veles - программы для автолюбителей, программы из раздела графика - программы для работы с фото, сделанными цифровым фотоаппаратом, программу Bricks - игрушку для детей и взрослых, программу записную книжку, программу TellMe - говорящий Русско-Английский разговорник - программу для тех, кто собирается погостить за бугром или повысить свои знания в английском, теоретический материал по программированию в среде Borland C++ Builder, C# (Windows приложения и ASP.Net Web сайты).

В начало страницы

К началу раздела

На главную страницу