Глава 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, 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. Класс BackgroundWorker2.1. BackgroundWorker - возможности и методыBackgroundWorker - (System.ComponentModel.BackgroundWorker) класс, предназначенный для создания и управления работой потоков. Он предоставляет следующие возможности:
Таким образом, при использовании BackgroundWorker нет необходимости включения try/catch в рабочий поток и есть возможность выдачи информации в основной поток без явного вызова Control.Invoke. BackgroundWorker предоставляет следующие основные методы:
Отметим, что ряд компонентов Windows Forms теперь поддерживает асинхронные операции. К таким компонентам, в частности, относятся компоненты SoundPlayer, PictureBox. Поскольку фоновая операция выполняется в отдельном потоке, в ее коде не допускаются манипуляции с элементами формы. Это ограничение связано с тем, что обработка сообщений происходит именно в том потоке, в котором изначально создан элемент. 2.2. Использование контрола BackgroundWorkerСоздадим простой проект решения (Рис.1.), поместив на форму пять контролов, четыре уже нам знакомых: Button, TextBox, Timer, ProgressBar и пятым, контрол со вкладки Components - BackgroundWorker.
Рис.1. Проект решения Установим свойства для контролов:
Для контрола 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.
Рис.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.
Рис.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.
Рис.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 с небольшим миллисекунд и выполнил свою задачу.
Рис.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; } } }
Рис.6. Результат выполнения кода 3.2. Синхронизация с использованием класса MonitorСинхронизация с использованием статического класса Monitor является одним из самых простых и удобных механизмов, позволяющим использовать практически любой объект, хранящийся в управляемой куче, для синхронизации доступа (достаточно его только объявить, можно нигде и не использовать в иных целях, кроме синхронизации). Перед выполнением синхронизируемого кода выполняется захват монитора, а по окончании - освобождение: Monitor.Enter(my_object); ...... Monitor.Exit(my_object); Механизм захвата заключается в том, что в момент захвата создается связанный с объектом специальный синхронизирующий элемент. При любой попытки нового захвата объекта другим потоком новый синхронизирующий элемент не может быть создан и поток приостанавливается до освобождения монитора (поток переходит в режим ожидания). В случае возникновения прерывания при выполнении критической операции есть вероятность того, что монитор не будет освобожден, поэтому рекомендуется использовать конструкции try/catch/finaly. Для демонстрации работы монитора воспользуемся кодом предыдущего примера. Внесем небольшие изменения:
Напомним, что таймер пытается запустить потоки каждые 100мс. Таким образом, потоки будут вновь запускаться практически сразу после своего окончания. Результат работы кода показан на Рис.7.
Рис.7. Результат выполнения кода Испытание кода проведено для четырех случаев, путем добавления и удаления помеченных как "//****" строк кода.
В заключении параграфа отметим, что в 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. Добавим всего три строчки кода :
Смысл добавленных строк - поток 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 представлено три вида таймеров:
Далее мы рассмотрим использование таймеров для решения задачи, постановка которой описана в п.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; } } Замечания по коду решения:
Приведенный код показывает, что сам таймер может создавать потоки. Но, используя данный таймер, можно выполнить запуск не только одиночной функции в фоновом потоке, но и поток, созданный при помощи класса 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; 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 void button1_Click(object sender, EventArgs e) { mytimer = new myTimer(200, 10000); 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; } } } Особенности реализации кода (выделено зеленым цветом):
Литература:
Молчанов Владислав 27.09.2007г. Еcли Вы пришли с поискового сервера - посетите мою главную страничкуНа главной странице Вы найдете программы комплекса Veles - программы для автолюбителей, программы из раздела графика - программы для работы с фото, сделанными цифровым фотоаппаратом, программу Bricks - игрушку для детей и взрослых, программу записную книжку, программу TellMe - говорящий Русско-Английский разговорник - программу для тех, кто собирается погостить за бугром или повысить свои знания в английском, теоретический материал по программированию в среде Borland C++ Builder, C# (Windows приложения и ASP.Net Web сайты). |