Организация интерфейса приложения требует выполнения нескольких правил:
Реализация первых двух правил понятна и хорошо описана, например в [9]. Сейчас нас будет интересовать реализация третьего правила, - как построить интерфейс, позволяющий наблюдать и управлять ходом выполнения процесса, реализующего бизнес-логику приложения.
При традиционном построении интерфейса следует выделять, собирая, например, в отдельный контейнер, входные параметры приложения, значения которых задает пользователь до запуска процесса на выполнение. В отдельный контейнер помещаются выходные данные процессы. И в простейшем случае визуальный интерфейс приложения прост: есть контейнеры "Вход", "Выход" и кнопка "Пуск", запускающая процесс, по окончании работы которого отображаются значения выходных параметров процесса. Когда все это реализуется в одном потоке, то, нажав кнопку "Пуск", нужно ждать окончания работы процесса. Если он, не дай бог зациклился, то единственное спасение - три заветные клавиши - Ctrl -Alt - Del.
В серьезных приложениях, требующих управления, необходимо выделять контейнеры для наблюдаемых и управляемых параметров. Наблюдаемые параметры - это те параметры, по значениям которых можно судить о том, как идет процесс, и следует ли применить к нему то или иное управляющее воздействие. Значения наблюдаемых параметров создаются процессом бизнес -логики и должны в момент их вычисления непосредственно отображаться в интерфейсе. Значения управляющих параметров задаются пользователем, управляющим процессом, и должны быть непосредственно восприняты управляемым процессом в момент их задания. Таким образом, процесс должен уметь "читать" значения управляющих параметров и "записывать" значения наблюдаемых параметров. Два процесса - управляющий и управляемый должны взаимодействовать и работать параллельно.
Для того чтобы реализовать такую схему работы приложения, его можно построить как многопоточное приложение, где интерфейс и бизнес логика выполняются в разных потоках. Как правило, основной поток реализует интерфейс приложения, а бизнес-логика реализуется в дочерних потоках. В хорошо построенных приложениях не должны возникать критические ситуации, характерные для многопоточных приложений, - гонка данных и клинч. Каждый дочерний процесс "пишет" значения наблюдаемых параметров в "свои" элементы интерфейса и "читает" значения управляющих параметров из соответствующих элементов управления. К сожалению, не все приложения "хорошо" построены и проблемы в многопоточных приложениях возникают.
При многопоточном программировании на C# элементы интерфейса не относятся к общим ресурсам, доступным для всех потоков. Поток не имеет права доступа к элементу интерфейса, созданному в другом потоке. Это означает, что дочерние потоки, реализующие бизнес-логику, не имеют права "чтения" и "записи" в элементы управления интерфейса, созданные в основном потоке. Организация взаимодействия между потоками, использующими общие элементы интерфейса, требует своего рода маршалинга, когда объект одного потока передается элементу управления другого потока, проходя нужные стадии преобразования. Такой маршалинг можно реализовать, используя метод Invoke.
Все элементы интерфейса обладают методом Invoke, наследуя его от класса Control. Метод перегружен и имеет две реализации:
Invoke(Delegate); Invoke(Delegate, object[]);
В обеих реализациях первый аргумент функционального типа (delegate) задает метод, принадлежащий потоку, в котором определен элемент интерфейса. Именно этот метод и будет выполнять операцию над элементом интерфейса. Если нет необходимости передавать методу информацию, то используется реализация Invoke с одним аргументом. В этом случае в качестве функционального типа можно использовать стандартный тип - класс MethodInvoker из пространства System.Windows.Forms. Рассмотрим пример вызова метода Invoke:
myForm.Invoke(new MethodInvoker(myForm.SetVisions));
Здесь объект myForm, задающий форму, вызывает метод Invoke. В качестве фактического параметра ему передается метод SetVisions - метод без аргументов, соответствующий типу MethodInvoker. Метод SetVisions принадлежит тому же классу, что и объект myForm и потому может быть вызван этим объектом.
Чаще всего, методу, работающему с элементом управления, необходимо передать информацию, так что метод, вызываемый в Invoke, может иметь один или несколько аргументов. В этом случае необходимо определить соответствующий функциональный тип - аналог MethodInvoker, создать метод - экземпляр этого типа, передать этот метод в качестве первого аргумента методу Invoke, а необходимые данные передать в качестве второго аргумента. Второй аргумент метода Invoke задается массивом объектов, таким образом можно передать значения всех требуемых аргументов методу, работающему с элементом управления. Рассмотрим пример, когда элементу управления нужно передать строку текста - объект типа string. Объявим прежде всего соответствующий функциональный тип:
public delegate void Delegate_Void_String(string par); Определим теперь экземпляр этого типа и свяжем с ним требуемый метод: public Delegate_Void_String myDelegate; myDelegate = AddList;
Метод AddList, добавляющий строку в элемент управления ListBox, определен следующим образом:
void AddList(string item)
{
listBoxValues.Items.Add(item);
}
Теперь в другом потоке можем добавить данные в элемент управления, вызвав метод Invoke следующим образом:
correctForm.Invoke(correctForm.myDelegate, new object[] { res });
Здесь res задает передаваемую строку данных, добавляемую в список элемента управления.
Метод Invoke является синхронным методом, ожидающим завершения операции над элементом управления. Методы BeginInvoke и EndInvoke обеспечивают асинхронное выполнение. По принципу организации передачи информации они схожи, и я не буду останавливаться подробно на их рассмотрении.
Рассмотрим теперь пример простого приложения, демонстрирующий корректную работу интерфейса, позволяющего управлять процессом вычислений. На следующем рисунке показан интерфейс нашего приложения:
(рис 8.1) Интерфейс приложения CorrectExample
Интерфейс построен в соответствии с описанной схемой. В контейнере "Наблюдение" размещен элемент ListBox, в котором по ходу вычислений будут отображаться значения вычисляемого параметра. В контейнере "Управление" находятся три командные кнопки. Кнопка "Пуск" позволяет запускать процесс вычислений. Кнопка "Стоп" позволяет в любой момент прервать вычисления. Кнопка "Очистить" позволяет очистить список наблюдаемых значений параметра. Рисунок показывает состояние проекта в момент вычислений. Можно заметить, что в этот момент кнопка "Очистить" отключена. Она включается только после того, как вычисления остановлены по нажатию кнопки "Стоп". При запуске процесса вычислений она снова переходит в режим "Выключена". Это гарантирует, что не возникнет ситуация, при которой два потока будут пытаться одновременно писать в список и пытаться его очистить.
Перейдем к описанию проекта. Начнем с бизнес-логики. Как и положено, бизнес-логика отделена от интерфейса и находится в отдельном классе, названном Worker. (Я не стал создавать DLL, чтобы не загромождать изложение деталями.)
Этот класс содержит, во-первых, ссылку на интерфейсный класс, необходимую для обеспечения взаимодействия. Во-вторых, в классе должны быть наблюдаемые и управляющие параметры, значениями которых класс Worker будет обмениваться с интерфейсным классом. Ну и наконец, в классе должен быть метод, реализующий соответствующий бизнес-процесс. В нашей модели все устроено достаточно просто.
namespace WindowsFormsCorrectExample
{
/// <summary>
/// Класс, реализующий бизнес-логику проекта
/// </summary>
class Worker
{
//Ссылка на интерфейсный класс
FormCorrect correctForm;
//Управляющая переменная, изменяемая в интерфейсе
bool stop = false;
Random rnd = new Random();
/// <summary>
/// Конструктор
/// </summary>
/// <param name="form">ссылка на интерфейсный класс</param>
public Worker(FormCorrect form)
{
correctForm = form;
}
/// <summary>
/// Доступ на запись управляющей переменной
/// </summary>
public bool Stop
{
set { stop = value; }
}
/// <summary>
/// Основной метод, реализующий логику проекта
/// В цикле моделируется значений наблюдаемого параметра,
/// передаваемого в список, отображаемый в интерфейсе проекта
/// Завершение цикла вычислений зависит от пользователя
/// и происходит при нажатии кнопки "Стоп"
/// </summary>
public void Run()
{
string res = "";
int i = 1;
while (!stop)
{
//Вычисление наблюдаемого параметра
res = i.ToString() + "." + rnd.Next(100).ToString();
i++;
//Наблюдаемый параметр передается основному потоку
correctForm.Invoke(correctForm.myDelegate, new object[] {res});
}
}
}
}
Класс снабжен комментариями, которых, надеюсь, с учетом ранее сделанных замечаний достаточно для понимания его работы. Главное, на что следует обратить внимание, это вызов метода Invoke для передачи очередного значения наблюдаемого параметра res для отображения в интерфейсе.
Рассмотрим теперь организацию интерфейсного класса. Деталей здесь больше. Начнем с обработчика центральной кнопки "Пуск", запускающей вычисления:
/// <summary>
/// Запуск вычислений в новом потоке
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void buttonStart_Click(object sender, EventArgs e)
{
buttonClear.Enabled = false;
Thread myThread = new Thread(Pusk);
myThread.Start();
}
Здесь создается новый поток и ему передается метод класса, выполняемый в этом потоке, - метод Pusk, не имеющий аргументов. Этот метод устроен также просто:
/// <summary>
/// Метод, выполняемый в потоке
/// Вызывает метод класса Worker, реализующий бизнес-логику
/// </summary>
void Pusk()
{
worker = new Worker(this);
worker.Run();
}
В задачу этого метода входит создание экземпляра класса Worker и вызов метода Run, выполняющего вычисления.
Завершение вычислений инициируется пользователем в тот момент, когда он нажимает на кнопку "Стоп". Обработчик этого события соответствующее управляющее воздействие передает объекту worker, изменяя значение управляющей переменной stop с false на true.
/// <summary>
/// управление вычислениями
/// Значение управляющей переменной передается объекту worker
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void buttonStop_Click(object sender, EventArgs e)
{
worker.Stop = true;
buttonClear.Enabled = true;
}
А теперь приведу текст класса в целом, опуская ранее приведенные фрагменты кода:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;
namespace WindowsFormsCorrectExample
{
public delegate void Delegate_Void_String(string par);
public partial class FormCorrect : Form
{
public Delegate_Void_String myDelegate;
Worker worker;
public FormCorrect()
{
InitializeComponent();
myDelegate = AddList;
}
/// <summary>…
private void buttonStart_Click(object sender, EventArgs e)…
/// <summary>…
void Pusk()…
/// <summary>…
void AddList(string item)…
/// <summary>…
private void buttonStop_Click(object sender, EventArgs e)…
private void buttonClear_Click(object sender, EventArgs e)
{
listBoxValues.Items.Clear();
}
}
}
Прежде чем закончить рассмотрение этого простого примера, сделаю одно важное замечание, поясняющее, как нужно понимать слова о том, что поток не может непосредственно обращаться к элементам интерфейса, созданным в другом потоке. Давайте посмотрим, что произойдет, если вместо вызова метода Invoke, упаковывающего обращение к методу, получающему доступ к элементу управления, будем непосредственно вызывать нужный нам метод. Заменим вызов:
correctForm.Invoke(correctForm.myDelegate, new object[] { res });
на вызов:
correctForm.myDelegate(res);
Если запустить проект в отладочном режиме, то умный отладчик обнаружит некорректное обращение к элементу управления из другого потока и выбросит исключение с соответствующим уведомлением. Если же запустить проект в исполняемом режиме (Ctrl + F5), то исключительная ситуация не возникает.
Давайте чуть усложним интерфейс проекта, добавив управляющие переменные, значения которых пользователь может задавать, не останавливая процесс вычислений, но воздействуя на получаемые результаты. Построим новый проект WindowsInterfaceAnd Threads, представляющий расширенный вариант проекта CorrectExample. Этот проект будет также содержать класс Worker, реализующий бизнес - логику, и интерфейсный класс. На следующем рисунке показан новый расширенный вариант интерфейса:
(рис 8.2) Расширенный вариант интерфейса проекта с управляющими переменными
В контейнере "Наблюдение" будут отображаться значения двух наблюдаемых параметров". В контейнере "Управление" появилась дополнительная кнопка "Изменить пределы и два текстовых окошка для задания значений управляющих переменных, названных нижним и верхним пределом. Пользователь в любой момент может изменить значения этих переменных, но вступят они в силу только после нажатия соответствующей кнопки. Было бы неразумно, если бы поток, осуществляющий вычисления, получал доступ к этим переменным параллельно с изменением их значений. Поэтому в таких ситуациях следует отделять изменение значений пользователем от момента вступления в силу измененных значений.
Данный проект во многом схож с ранее описанным проектом CorrectExample. Здесь также действуют два класса - интерфейсный класс, названный FormSeeAndControl, и класс Worker, реализующий бизнес-логику. По этой причине проект подробно описывать не буду, оставляя его полную реализацию читателям. Остановлюсь лишь на некоторых деталях. Поскольку значения управляющих параметров задаются пользователем, то ввод необходимо контролировать (значения должны быть целыми числами, в определенном диапазоне, нижний предел должен быть меньше верхнего и так далее). Что делать, если условия нарушаются? Можно было бы вместе с выдачей соответствующего сообщения (окно для выдачи сообщения предусмотрено) останавливать процесс, ожидая исправления данных. В данном варианте вычисления продолжаются со значениями, принятыми по умолчанию.
На одном отличии этого проекта от предыдущего остановлюсь подробнее, поскольку оно важно для разработки любых проектов с управляющим интерфейсом. В предыдущем проекте поток, которому требовалось передать данные элементу интерфейса, в методе Invoke задавал два аргумента - метод и данные. Это требовало задания специального делегата, описывающего сигнатуру метода. Можно поступать по-другому, используя технику, типичную для объектно-ориентированного программирования. Можно иметь в классе соответствующие поля, методы берут значения из этих полей, а потому не имеют никаких аргументов - они работают только с полями класса. В этом случае все вызовы Invoke можно свести к универсальной стандартной схеме - вызову метода без аргументов, сигнатура которого удовлетворяет стандартному типу - MethodInvoker. В данном проекте все так и делается. Для управляющих переменных в интерфейсном классе введены поля:
int lowLimit, highLimit;
В классе Worker у них другие имена:
int minLimit= 0, maxLimit = 10;
Поля для наблюдаемых переменных в классе Worker имеют имена:
string val = ""; string quality ="";
Поля закрыты, но заданы открытые свойства, позволяющие клиентам класса получать доступ к полям.
Метод Run теперь выглядит так:
/// <summary>
/// Метод, осуществляющий процесс работы
/// </summary>
public void Run()
{
GetControlParams();
while (!stop)
{
if (change)
{
GetControlParams();
change = false;
}
SetVisionParams();
}
}
По ходу работы метод Run вызывает два метода - один для получения значений управляющих параметров, другой для передачи значений наблюдаемых параметров. Для управляемых переменных вначале вызывается через Invoke метод, читающий их значения в поля интерфейсного класса. Потом эти значения переписываются в поля класса Worker:
/// <summary>
/// Читает значения управляющих параметров
/// </summary>
void GetControlParams()
{
myForm.Invoke(new MethodInvoker(myForm.GetLimits));
minLimit = myForm.LowLimit;
maxLimit = myForm.HighLimit;
}
Наблюдаемые значения создаются, их значения записываются в поля класса, а затем через Invoke вызывается метод интерфейсного класса, отображающий значения, хранимые в полях, в соответствующих элементах интерфейса:
/// <summary>
/// Пишет значения наблюдаемых параметров
/// </summary>
public void SetVisionParams()
{
int vali = rnd.Next(minLimit, maxLimit + 1);
if (vali == minLimit)
quality = EQuality.Ниже_Нормы.ToString();
else if (vali == maxLimit)
quality = EQuality.Выше_нормы.ToString();
else
quality = EQuality.Норма.ToString();
numer++;
val = numer + "." + vali;
quality = numer + "." + quality;
myForm.Invoke(new MethodInvoker(myForm.SetVisions));
}
Методы интерфейсного класса GetLimits и SetVisions описывать не буду, оставляя их для самостоятельного рассмотрения. Еще раз обращаю внимание на то, что переход к использованию класса MethodInvoker делает схему работы не только универсальной, но и более эффективной по времени реализации, так что следует пользоваться полями класса для передачи информации.
Взаимодействие между управляющим процессом и управляемым можно организовать разными способами. Рассмотрим три основных способа:
F, описывающий управляемый процесс, содержит ссылку на управляющий объект - объект класса G. В свою очередь управляющий класс G содержит ссылку на управляемый объект - объект класса F.
F генерирует событие, а управляемый объект его обрабатывает. В свою очередь управляемый объект класса G генерирует событие, а управляющий объект его обрабатывает.
Что происходит, когда управляющий и управляемый процесс находятся в разных потоках? Все три способа взаимодействия осуществимы и в этом случае. Но когда управляющий процесс представлен интерфейсным классом, то возникает особенность, уже рассмотренная нами. Другим потокам запрещается напрямую обращаться к элементам управления интерфейсного класса, работающего в другом потоке.
Взаимодействие, построенное на основе механизма ссылок, управляющего интерфейсного класса и управляемого класса, реализующего бизнес - логику, подробно рассмотрено на примере проектов CorrectExample и WindowsInterfaceAndThreads. Справиться с проблемой доступа к элементам интерфейса из другого потока позволяет вызов метода Invoke, которым обладают все элементы интерфейса.
Недостаток схемы взаимодействия на основе ссылок состоит в том, что не только интерфейсный класс содержит ссылку на класс, реализующий бизнес - логику, но и класс бизнес - логики должен содержать ссылку на интерфейсный класс. Это противоречит правилам хорошего программирования, - бизнес - логика не должна привязываться к фиксированному интерфейсу. Схема, построенная на событиях, где объект, зажигающий событие, не знает, какие объекты и каких классов будут обрабатывать это событие, представляется предпочтительней. Но возможно ли зажигать событие в одном потоке, а обрабатывать его в другом потоке? Давайте рассмотрим подробнее эту схему.
Рассмотрим пример взаимодействия интерфейсного класса и класса бизнес - логики, основанное на событиях. Наша цель состоит в том, чтобы класс бизнес логики не содержал ссылку на интерфейсный класс. Интерфейсный класс, являющийся управляющим классом, конечно же, содержит ссылки на объекты, которыми он управляет. В тот момент, когда объект бизнес - логики вырабатывает новое значение параметра, принадлежащего к наблюдаемым параметрам, он будет, зажигая соответствующее событие, передавать это параметр всем объектам, принимающим событие. В интерфейсном классе, подписанном на получение сообщения о событии, событие может быть должным образом обработано.
Модифицируем наш последний пример. По аналогии с проектом WindowsInterfaceAndThreads построим проект WindowsFormsInterfaceAndEvents. Введем в рассмотрение класс Worker_Ev, который в отличие от ранее рассмотренного класса Worker, не будет содержать ссылку на интерфейсный класс, но будет включать событие, позволяющее уведомить интерфейсный класс о выработке новых значений наблюдаемых параметров.
Напомню, общую схему построения класса с событиями. Первым делом нужно объявить делегат, описывающий сигнатуру события. Принято, чтобы сигнатура события задавалась двумя параметрами, первый из которых задает объект, возбуждающий событие, а второй параметр представлял объект некоторого класса, наследуемого от класса EventArgs, содержащего параметры, передаваемые обработчикам события. В класс, вырабатывающий событие, нужно добавить объявление события с соответствующей сигнатурой, метод OnFire, зажигающий событие, и в нужных местах, где событие возникает, вызывать этот метод. В классах, которые хотят подписаться на получение сообщения о событии, следует подключить к событию обработчик события, имеющий соответствующую событию сигнатуру. Подробнее об этом можно прочесть в [9].
Вот как выглядит описание делегата, задающего сигнатуру события:
public delegate void ResultEventHandler(object sender, ResultEventArgs args);
Класс ResultEventArgs, описывающий параметры, передаваемые обработчикам события, выглядит так:
public class ResultEventArgs : EventArgs
{
/// <summary>
/// наблюдаемые параметры представляют
/// входные аргументы, передаваемые обработчику события
/// </summary>
string val, quality;
public string Val
{
get { return val; }
}
public string Quality
{
get { return quality; }
}
public ResultEventArgs(string val, string quality)
{
this.val = val;
this.quality = quality;
}
}
Класс Worker_Ev не содержит ссылки на интерфейсный класс, но содержит поле, задающее событие:
public event ResultEventHandler result_event;
Метод этого класса, которому обычно дается имя OnFire имеет стандартный вид:
/// <summary>
/// "Зажигает" события
/// </summary>
/// <param name="args">аргументы, передаваемые обработчику</param>
public void OnFire(ResultEventArgs args)
{
if(result_event != null)
result_event(this, args);
}
Метод, моделирующий процесс бизнес-логики, достаточно прост:
/// <summary>
/// Метод, осуществляющий процесс работы
/// </summary>
public void Run()
{
while (!stop)
{
SetVisionParams();
}
}
Переменная stop - это управляемая переменная. Когда в интерфейсном классе будет нажата соответствующая кнопка, выполнение цикла в Run завершится. Метод SetVisionParams вырабатывает значения наблюдаемых параметров, которые должны передаваться управляющему процессу через механизм событий. Вот текст этого метода:
/// <summary>
/// Создает значения наблюдаемых параметров
/// Зажигает событие
/// </summary>
public void SetVisionParams()
{
int val_i;
val_i = rnd.Next(minLimit, maxLimit + 1);
if (val_i == minLimit)
quality = EQuality.Ниже_Нормы.ToString();
else if (val_i == maxLimit)
quality = EQuality.Выше_нормы.ToString();
else
quality = EQuality.Норма.ToString();
numer++;
val = numer + "." + val_i;
quality = numer + "." + quality;
//зажигаем событие - получены результаты
OnFire(new ResultEventArgs(val, quality));
}
Интерфейсный класс, поле которого содержит ссылку worker, при запуске процесса бизнес - логики, создает этот объект и присоединяет к нему обработчик этого события:
private void buttonStart_Click(object sender, EventArgs e)
{
buttonClear.Enabled = false;
textBoxMessage.Text = "";
// Создаем объект бизнес-логики
worker = new Worker_Ev();
//Присоединяем обработчик события объекта worker
worker.result_event += new ResultEventHandler(SetVisions_Event);
//Устанавливаем границы
GetLimits();
//Создаем дочерний поток для выполения процесса бизнес-логики
Thread workerThread = new Thread(worker.Run);
//запускаем процесс
workerThread.Start();
}
Когда при работе процесса бизнес - логики в другом потоке, вырабатываются значения наблюдаемых параметров и зажигается событие, то в интерфейсном классе, получившем сообщение о событии, вызывается обработчик события:
/// <summary>
/// запись наблюдаемых параметров
/// в интерфейсные элементы
/// </summary>
public void SetVisions_Event(object sender, ResultEventArgs args)
{
listBoxValues.Items.Add(args.Val);
listBoxValues.Refresh();
listBoxQuality.Items.Add(args.Quality);
listBoxQuality.Refresh();
}
В данном обработчике события запись наблюдаемых параметров производится непосредственно в элементы интерфейса - элементы listBox. Все хорошо работает, если запускать Release версию приложения (Ctrl + F5). Но в Debug версии, при работе в отладочном режиме возникает исключительная ситуация, показанная на следующем рисунке:
(рис 8.3) Запрещенный доступ к элементам интерфейса из другого потока
Когда в приложении есть только два потока - один для интерфейса, другой для бизнес - логики, то работать без вызова метода Invoke безопасно. Но, конечно же, возникновение исключительной ситуации для отладочной версии не годится для профессиональной разработки. Необходимо найти другой способ работы в схеме событий.
Решается эта проблема достаточно просто. Записывать данные в элементы интерфейсного класса из другого потока не разрешается, но в поля этого класса запись разрешена. Поэтому вполне возможно создать в интерфейсном классе контейнер, куда будут записываться значения наблюдаемых параметров. Достаточно теперь в интерфейсном классе иметь командную кнопку, по нажатии которой на законных основаниях данные из контейнера будут переноситься в элементы интерфейса. Реализуем эту стратегию.
Обработчик события SetVisionsEvent, который приводил к исключительной ситуации, запишем теперь в следующем виде:
/// <summary>
/// запись наблюдаемых параметров
/// в контейнеры (списки)
/// </summary>
public void SetVisions_Event(object sender, ResultEventArgs args)
{
list_val.Add(args.Val);
list_quality.Add(args.Quality);
}
Здесь list_val и list_quality - два контейнера - два списка, в которые обработчик события помещает наблюдаемые параметры. В интерфейс проекта добавлена командная кнопка, позволяющая в нужный момент переносить данные из контейнеров в соответствующие элементы интерфейса, - списки, отображаемые на экране:
/// <summary>
/// перенос данных из контейнеров
/// в отображаемые элементы интерфейса
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void buttonDataSet_Click(object sender, EventArgs e)
{
for (int i = 0; i < list_val.Count; i++)
{
listBoxValues.Items.Add(list_val[i]);
listBoxQuality.Items.Add(list_quality[i]);
}
}
Заметьте, все эти изменения в интерфейсном классе, никак не отразились на классе Worker_Ev, генерирующем события.
Вот как выглядит теперь интерфейс нашего модифицированного проекта, обеспечивающего взаимодействие, основанное на событиях:
(рис 8.4) Интерфейс взаимодействия, основанного на событиях
Рассмотрим теперь более интересный проект, реализующий известную игру 15. Коротко об игре. Игра ведется на квадратном поле n * n. В классическом варианте n = 4. На этом поле в исходном состоянии размещается 15 квадратов, на каждом из которых написано одно из чисел - от 1 до 15. Порядок размещения квадратов случаен. Одно поле остается свободным. Цель игры в том, чтобы передвигая квадраты, используя одно свободное поле, упорядочить как можно больше квадратиков. В половине случаев удается правильно поставить все 15 квадратов. Но не для всякой начальной расстановки можно добиться этого результата. Гарантировать можно правильную расстановку лишь 13 квадратов. Математический анализ игры показал, что по начальной расстановке можно однозначно определить, достижимо ли полное упорядочение. Для этого достаточно подсчитать число нарушений порядка в исходной расстановке. Если оно нечетно, то правильно расставить можно лишь 13 квадратов.
Реализация этой игры представляет хорошее упражнение для программиста. На рис. 8.5 показан интерфейс проекта, реализующего игру 15:
(рис 8.5) Начальное состояние игры 15
Командная кнопка "Перемешать" позволяет задать новую случайную расстановку. Меню "Файл" позволяет сохранить расстановку или задать расстановку, записанную в файле. Меню "Игра" позволяет управлять свойствами игры, можно изменять скорость игры и менять игрока (человек или компьютер). Командные кнопки "Пуск" и "Останов" позволяют запустить или приостановить игру. Ряд элементов управления в интерфейсе позволяют отображать наблюдаемые параметры игры. В списке справа запоминается начальная расстановка, и записываются ходы, сделанные компьютером. Естественно, что каждый ход отображается в игровом поле. Еще два элемента сохраняют информацию о числе уже упорядоченных квадратов. Один элемент управления представляет текстовое поле, другой (ProgressBar) эту же информацию представляет в графическом виде. Еще одно поле служит для выдачи текстовых сообщений о ходе игры.
Игра представляет хороший образец на тему взаимодействия интерфейса и бизнес - логики. Здесь есть наблюдаемые параметры и управляемые параметры, которые можно менять по ходу игры (менять скорость, останавливать игру, продолжать выполнение, менять игрока).
Проект, реализующий эту игру, основан по схеме взаимодействия интерфейса и бизнес - логики, работающих в разных потоках, на основе взаимных ссылок. В данной ситуации это естественный вариант, поскольку игра рассчитана на вполне определенный интерфейс. При записи значений наблюдаемых параметров в соответствующие элементы интерфейса использовались методы Invoke.
Проект достаточно большой, поэтому приводить текст его не буду, но также как и все проекты, сопровождающие текст этого курса, как и сам курс, надеюсь, будет доступен на сайте Интернет университета ИТ (intuit.ru).
В завершении приведу снимок заключительного состояния игры, для случая, когда начальная расстановка позволяет правильно расставить все 15 квадратов:
(рис 8.6)
Подведем некоторые итоги. Все задачи, для решения которых мы пишем программы, можно условно разделить на пять классов:
Одной из целей учебного курса являлось рассмотрение широкого круга задач, позволяющее отнести задачу к тому или иному классу. При обосновании классификации можно использовать результаты анализа численных экспериментов, полученные при запуске программных проектов, создаваемых автором в ходе написания курса.
Программные проекты фактически являются частью курса, его дополнением, его поддержкой. Фрагменты проектов широко использовались в тексте курса. Тем не менее для программистов программный код не менее интересен, чем словесное описание. Исполняемому коду можно верить больше, чем словам. Поэтому все проекты, созданные автором для поддержки этого курса, с их кратким описанием будут доступны в интернете прежде всего на сайте открытого Интернет Университета ИТ – intuit.ru и на сайте НПО "Центрпрограммсистем" - cps.tver.ru
В качестве примера классификации рассмотрим задачу суммирования, широко обсуждаемую в учебном курсе. К какому из классов ее следует отнести? Для обоснования выводов был создан программный проект, а точнее, Решение (Solution) Sum, содержащее 5 проектов – DLL и 4 интерфейсных проекта.
В проектах рассматриваются три варианта вычисления суммы $$S= \sum_k a_k$$ , где $$a_k$$ это :
Начнем с задачи суммирования элементов массива. Вот результаты, полученные на моем компьютере (64-х битный компьютер с 6 Гб оперативной памяти и 4-мя физическими ядрами) при запуске Windows проекта из Решения Sum:
Как можно видеть, все алгоритмы – последовательный и параллельные - прекрасно справляются с задачей. Время суммирования вплоть до массива, содержащего десять миллионов элементов, составляет менее 0,1 секунды. В такой ситуации параллельные алгоритмы не имеют никакого преимущества перед последовательным алгоритмом. Так что задачу нахождения суммы элементов массива, также как нахождение максимального элемента и другие подобные ей задачи, следует отнести ко второму классу, где применение параллельных алгоритмов хотя и возможно, но нецелесообразно на компьютерах подобного класса.
Приведу теперь результаты экспериментов для задачи суммирования бесконечного сходящегося ряда на примере вычисления значений функции Arcsin(x). Эта задача интересна еще и тем, что эффективный последовательный алгоритм использует рекуррентное соотношение для вычисления очередного члена суммы. При распараллеливании такого эффективного приема не существует. Рекуррентное соотношение хотя и можно построить для параллельного шагового алгоритма, но оно значительно сложнее в сравнении с последовательным вариантом. Учитывая еще, что последовательный алгоритм практически мгновенно решает эту задачу, то у параллельного алгоритма нет шансов выиграть, что и подтверждается результатами экспериментов:
Можно видеть, что как последовательный, так и параллельный алгоритм прекрасно справляются с задачей. Время многократного (10000 повторов) вычисления значения функции составляет менее 0,1 секунды. Но опять-таки параллельный алгоритм не имеет никакого преимущества перед последовательным алгоритмом. Так что и эту задачу следует отнести ко второму классу, где применение параллельных алгоритмов хотя и возможно, но нецелесообразно на компьютерах подобного класса.
Полагаю, что справедлива следующая гипотеза:
Задачи с линейной временной сложностью относятся ко второму классу.
Для того чтобы параллельные алгоритмы выигрывали у последовательных алгоритмов исходная задача должно иметь по крайней мере квадратичную сложность. Суммирование конечного ряда, где каждый член ряда требует вычисления значения функции с линейной сложностью O(n), относится к подобным задачам. Вот как выглядят результаты эксперимента:
Рассматриваемую нами задачу, имеющую квадратичную временную сложность O(n2) следует отнести к третьему классу задач, допускающих эффективное распараллеливание.
Можно сделать и более общий вывод. Для потенциально распараллеливаемых задач со сложностью O(n2) и выше следует искать эффективные параллельные алгоритмы, позволяющие ощутимо сократить время решения задачи. Поиск таких алгоритмов не простая задача.
Организация интерфейса приложения требует выполнения нескольких правил:
Реализация первых двух правил понятна и хорошо описана, например в [9]. Сейчас нас будет интересовать реализация третьего правила, - как построить интерфейс, позволяющий наблюдать и управлять ходом выполнения процесса, реализующего бизнес-логику приложения.
При традиционном построении интерфейса следует выделять, собирая, например, в отдельный контейнер, входные параметры приложения, значения которых задает пользователь до запуска процесса на выполнение. В отдельный контейнер помещаются выходные данные процессы. И в простейшем случае визуальный интерфейс приложения прост: есть контейнеры "Вход", "Выход" и кнопка "Пуск", запускающая процесс, по окончании работы которого отображаются значения выходных параметров процесса. Когда все это реализуется в одном потоке, то, нажав кнопку "Пуск", нужно ждать окончания работы процесса. Если он, не дай бог зациклился, то единственное спасение - три заветные клавиши - Ctrl -Alt - Del.
В серьезных приложениях, требующих управления, необходимо выделять контейнеры для наблюдаемых и управляемых параметров. Наблюдаемые параметры - это те параметры, по значениям которых можно судить о том, как идет процесс, и следует ли применить к нему то или иное управляющее воздействие. Значения наблюдаемых параметров создаются процессом бизнес -логики и должны в момент их вычисления непосредственно отображаться в интерфейсе. Значения управляющих параметров задаются пользователем, управляющим процессом, и должны быть непосредственно восприняты управляемым процессом в момент их задания. Таким образом, процесс должен уметь "читать" значения управляющих параметров и "записывать" значения наблюдаемых параметров. Два процесса - управляющий и управляемый должны взаимодействовать и работать параллельно.
Для того чтобы реализовать такую схему работы приложения, его можно построить как многопоточное приложение, где интерфейс и бизнес логика выполняются в разных потоках. Как правило, основной поток реализует интерфейс приложения, а бизнес-логика реализуется в дочерних потоках. В хорошо построенных приложениях не должны возникать критические ситуации, характерные для многопоточных приложений, - гонка данных и клинч. Каждый дочерний процесс "пишет" значения наблюдаемых параметров в "свои" элементы интерфейса и "читает" значения управляющих параметров из соответствующих элементов управления. К сожалению, не все приложения "хорошо" построены и проблемы в многопоточных приложениях возникают.
При многопоточном программировании на C# элементы интерфейса не относятся к общим ресурсам, доступным для всех потоков. Поток не имеет права доступа к элементу интерфейса, созданному в другом потоке. Это означает, что дочерние потоки, реализующие бизнес-логику, не имеют права "чтения" и "записи" в элементы управления интерфейса, созданные в основном потоке. Организация взаимодействия между потоками, использующими общие элементы интерфейса, требует своего рода маршалинга, когда объект одного потока передается элементу управления другого потока, проходя нужные стадии преобразования. Такой маршалинг можно реализовать, используя метод Invoke.
Все элементы интерфейса обладают методом Invoke, наследуя его от класса Control. Метод перегружен и имеет две реализации:
Invoke(Delegate); Invoke(Delegate, object[]);
В обеих реализациях первый аргумент функционального типа (delegate) задает метод, принадлежащий потоку, в котором определен элемент интерфейса. Именно этот метод и будет выполнять операцию над элементом интерфейса. Если нет необходимости передавать методу информацию, то используется реализация Invoke с одним аргументом. В этом случае в качестве функционального типа можно использовать стандартный тип - класс MethodInvoker из пространства System.Windows.Forms. Рассмотрим пример вызова метода Invoke:
myForm.Invoke(new MethodInvoker(myForm.SetVisions));
Здесь объект myForm, задающий форму, вызывает метод Invoke. В качестве фактического параметра ему передается метод SetVisions - метод без аргументов, соответствующий типу MethodInvoker. Метод SetVisions принадлежит тому же классу, что и объект myForm и потому может быть вызван этим объектом.
Чаще всего, методу, работающему с элементом управления, необходимо передать информацию, так что метод, вызываемый в Invoke, может иметь один или несколько аргументов. В этом случае необходимо определить соответствующий функциональный тип - аналог MethodInvoker, создать метод - экземпляр этого типа, передать этот метод в качестве первого аргумента методу Invoke, а необходимые данные передать в качестве второго аргумента. Второй аргумент метода Invoke задается массивом объектов, таким образом можно передать значения всех требуемых аргументов методу, работающему с элементом управления. Рассмотрим пример, когда элементу управления нужно передать строку текста - объект типа string. Объявим прежде всего соответствующий функциональный тип:
public delegate void Delegate_Void_String(string par); Определим теперь экземпляр этого типа и свяжем с ним требуемый метод: public Delegate_Void_String myDelegate; myDelegate = AddList;
Метод AddList, добавляющий строку в элемент управления ListBox, определен следующим образом:
void AddList(string item)
{
listBoxValues.Items.Add(item);
}
Теперь в другом потоке можем добавить данные в элемент управления, вызвав метод Invoke следующим образом:
correctForm.Invoke(correctForm.myDelegate, new object[] { res });
Здесь res задает передаваемую строку данных, добавляемую в список элемента управления.
Метод Invoke является синхронным методом, ожидающим завершения операции над элементом управления. Методы BeginInvoke и EndInvoke обеспечивают асинхронное выполнение. По принципу организации передачи информации они схожи, и я не буду останавливаться подробно на их рассмотрении.
Рассмотрим теперь пример простого приложения, демонстрирующий корректную работу интерфейса, позволяющего управлять процессом вычислений. На следующем рисунке показан интерфейс нашего приложения:
(рис 8.1) Интерфейс приложения CorrectExample
Интерфейс построен в соответствии с описанной схемой. В контейнере "Наблюдение" размещен элемент ListBox, в котором по ходу вычислений будут отображаться значения вычисляемого параметра. В контейнере "Управление" находятся три командные кнопки. Кнопка "Пуск" позволяет запускать процесс вычислений. Кнопка "Стоп" позволяет в любой момент прервать вычисления. Кнопка "Очистить" позволяет очистить список наблюдаемых значений параметра. Рисунок показывает состояние проекта в момент вычислений. Можно заметить, что в этот момент кнопка "Очистить" отключена. Она включается только после того, как вычисления остановлены по нажатию кнопки "Стоп". При запуске процесса вычислений она снова переходит в режим "Выключена". Это гарантирует, что не возникнет ситуация, при которой два потока будут пытаться одновременно писать в список и пытаться его очистить.
Перейдем к описанию проекта. Начнем с бизнес-логики. Как и положено, бизнес-логика отделена от интерфейса и находится в отдельном классе, названном Worker. (Я не стал создавать DLL, чтобы не загромождать изложение деталями.)
Этот класс содержит, во-первых, ссылку на интерфейсный класс, необходимую для обеспечения взаимодействия. Во-вторых, в классе должны быть наблюдаемые и управляющие параметры, значениями которых класс Worker будет обмениваться с интерфейсным классом. Ну и наконец, в классе должен быть метод, реализующий соответствующий бизнес-процесс. В нашей модели все устроено достаточно просто.
namespace WindowsFormsCorrectExample
{
/// <summary>
/// Класс, реализующий бизнес-логику проекта
/// </summary>
class Worker
{
//Ссылка на интерфейсный класс
FormCorrect correctForm;
//Управляющая переменная, изменяемая в интерфейсе
bool stop = false;
Random rnd = new Random();
/// <summary>
/// Конструктор
/// </summary>
/// <param name="form">ссылка на интерфейсный класс</param>
public Worker(FormCorrect form)
{
correctForm = form;
}
/// <summary>
/// Доступ на запись управляющей переменной
/// </summary>
public bool Stop
{
set { stop = value; }
}
/// <summary>
/// Основной метод, реализующий логику проекта
/// В цикле моделируется значений наблюдаемого параметра,
/// передаваемого в список, отображаемый в интерфейсе проекта
/// Завершение цикла вычислений зависит от пользователя
/// и происходит при нажатии кнопки "Стоп"
/// </summary>
public void Run()
{
string res = "";
int i = 1;
while (!stop)
{
//Вычисление наблюдаемого параметра
res = i.ToString() + "." + rnd.Next(100).ToString();
i++;
//Наблюдаемый параметр передается основному потоку
correctForm.Invoke(correctForm.myDelegate, new object[] {res});
}
}
}
}
Класс снабжен комментариями, которых, надеюсь, с учетом ранее сделанных замечаний достаточно для понимания его работы. Главное, на что следует обратить внимание, это вызов метода Invoke для передачи очередного значения наблюдаемого параметра res для отображения в интерфейсе.
Рассмотрим теперь организацию интерфейсного класса. Деталей здесь больше. Начнем с обработчика центральной кнопки "Пуск", запускающей вычисления:
/// <summary>
/// Запуск вычислений в новом потоке
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void buttonStart_Click(object sender, EventArgs e)
{
buttonClear.Enabled = false;
Thread myThread = new Thread(Pusk);
myThread.Start();
}
Здесь создается новый поток и ему передается метод класса, выполняемый в этом потоке, - метод Pusk, не имеющий аргументов. Этот метод устроен также просто:
/// <summary>
/// Метод, выполняемый в потоке
/// Вызывает метод класса Worker, реализующий бизнес-логику
/// </summary>
void Pusk()
{
worker = new Worker(this);
worker.Run();
}
В задачу этого метода входит создание экземпляра класса Worker и вызов метода Run, выполняющего вычисления.
Завершение вычислений инициируется пользователем в тот момент, когда он нажимает на кнопку "Стоп". Обработчик этого события соответствующее управляющее воздействие передает объекту worker, изменяя значение управляющей переменной stop с false на true.
/// <summary>
/// управление вычислениями
/// Значение управляющей переменной передается объекту worker
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void buttonStop_Click(object sender, EventArgs e)
{
worker.Stop = true;
buttonClear.Enabled = true;
}
А теперь приведу текст класса в целом, опуская ранее приведенные фрагменты кода:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;
namespace WindowsFormsCorrectExample
{
public delegate void Delegate_Void_String(string par);
public partial class FormCorrect : Form
{
public Delegate_Void_String myDelegate;
Worker worker;
public FormCorrect()
{
InitializeComponent();
myDelegate = AddList;
}
/// <summary>…
private void buttonStart_Click(object sender, EventArgs e)…
/// <summary>…
void Pusk()…
/// <summary>…
void AddList(string item)…
/// <summary>…
private void buttonStop_Click(object sender, EventArgs e)…
private void buttonClear_Click(object sender, EventArgs e)
{
listBoxValues.Items.Clear();
}
}
}
Прежде чем закончить рассмотрение этого простого примера, сделаю одно важное замечание, поясняющее, как нужно понимать слова о том, что поток не может непосредственно обращаться к элементам интерфейса, созданным в другом потоке. Давайте посмотрим, что произойдет, если вместо вызова метода Invoke, упаковывающего обращение к методу, получающему доступ к элементу управления, будем непосредственно вызывать нужный нам метод. Заменим вызов:
correctForm.Invoke(correctForm.myDelegate, new object[] { res });
на вызов:
correctForm.myDelegate(res);
Если запустить проект в отладочном режиме, то умный отладчик обнаружит некорректное обращение к элементу управления из другого потока и выбросит исключение с соответствующим уведомлением. Если же запустить проект в исполняемом режиме (Ctrl + F5), то исключительная ситуация не возникает.
Давайте чуть усложним интерфейс проекта, добавив управляющие переменные, значения которых пользователь может задавать, не останавливая процесс вычислений, но воздействуя на получаемые результаты. Построим новый проект WindowsInterfaceAnd Threads, представляющий расширенный вариант проекта CorrectExample. Этот проект будет также содержать класс Worker, реализующий бизнес - логику, и интерфейсный класс. На следующем рисунке показан новый расширенный вариант интерфейса:
(рис 8.2) Расширенный вариант интерфейса проекта с управляющими переменными
В контейнере "Наблюдение" будут отображаться значения двух наблюдаемых параметров". В контейнере "Управление" появилась дополнительная кнопка "Изменить пределы и два текстовых окошка для задания значений управляющих переменных, названных нижним и верхним пределом. Пользователь в любой момент может изменить значения этих переменных, но вступят они в силу только после нажатия соответствующей кнопки. Было бы неразумно, если бы поток, осуществляющий вычисления, получал доступ к этим переменным параллельно с изменением их значений. Поэтому в таких ситуациях следует отделять изменение значений пользователем от момента вступления в силу измененных значений.
Данный проект во многом схож с ранее описанным проектом CorrectExample. Здесь также действуют два класса - интерфейсный класс, названный FormSeeAndControl, и класс Worker, реализующий бизнес-логику. По этой причине проект подробно описывать не буду, оставляя его полную реализацию читателям. Остановлюсь лишь на некоторых деталях. Поскольку значения управляющих параметров задаются пользователем, то ввод необходимо контролировать (значения должны быть целыми числами, в определенном диапазоне, нижний предел должен быть меньше верхнего и так далее). Что делать, если условия нарушаются? Можно было бы вместе с выдачей соответствующего сообщения (окно для выдачи сообщения предусмотрено) останавливать процесс, ожидая исправления данных. В данном варианте вычисления продолжаются со значениями, принятыми по умолчанию.
На одном отличии этого проекта от предыдущего остановлюсь подробнее, поскольку оно важно для разработки любых проектов с управляющим интерфейсом. В предыдущем проекте поток, которому требовалось передать данные элементу интерфейса, в методе Invoke задавал два аргумента - метод и данные. Это требовало задания специального делегата, описывающего сигнатуру метода. Можно поступать по-другому, используя технику, типичную для объектно-ориентированного программирования. Можно иметь в классе соответствующие поля, методы берут значения из этих полей, а потому не имеют никаких аргументов - они работают только с полями класса. В этом случае все вызовы Invoke можно свести к универсальной стандартной схеме - вызову метода без аргументов, сигнатура которого удовлетворяет стандартному типу - MethodInvoker. В данном проекте все так и делается. Для управляющих переменных в интерфейсном классе введены поля:
int lowLimit, highLimit;
В классе Worker у них другие имена:
int minLimit= 0, maxLimit = 10;
Поля для наблюдаемых переменных в классе Worker имеют имена:
string val = ""; string quality ="";
Поля закрыты, но заданы открытые свойства, позволяющие клиентам класса получать доступ к полям.
Метод Run теперь выглядит так:
/// <summary>
/// Метод, осуществляющий процесс работы
/// </summary>
public void Run()
{
GetControlParams();
while (!stop)
{
if (change)
{
GetControlParams();
change = false;
}
SetVisionParams();
}
}
По ходу работы метод Run вызывает два метода - один для получения значений управляющих параметров, другой для передачи значений наблюдаемых параметров. Для управляемых переменных вначале вызывается через Invoke метод, читающий их значения в поля интерфейсного класса. Потом эти значения переписываются в поля класса Worker:
/// <summary>
/// Читает значения управляющих параметров
/// </summary>
void GetControlParams()
{
myForm.Invoke(new MethodInvoker(myForm.GetLimits));
minLimit = myForm.LowLimit;
maxLimit = myForm.HighLimit;
}
Наблюдаемые значения создаются, их значения записываются в поля класса, а затем через Invoke вызывается метод интерфейсного класса, отображающий значения, хранимые в полях, в соответствующих элементах интерфейса:
/// <summary>
/// Пишет значения наблюдаемых параметров
/// </summary>
public void SetVisionParams()
{
int vali = rnd.Next(minLimit, maxLimit + 1);
if (vali == minLimit)
quality = EQuality.Ниже_Нормы.ToString();
else if (vali == maxLimit)
quality = EQuality.Выше_нормы.ToString();
else
quality = EQuality.Норма.ToString();
numer++;
val = numer + "." + vali;
quality = numer + "." + quality;
myForm.Invoke(new MethodInvoker(myForm.SetVisions));
}
Методы интерфейсного класса GetLimits и SetVisions описывать не буду, оставляя их для самостоятельного рассмотрения. Еще раз обращаю внимание на то, что переход к использованию класса MethodInvoker делает схему работы не только универсальной, но и более эффективной по времени реализации, так что следует пользоваться полями класса для передачи информации.
Взаимодействие между управляющим процессом и управляемым можно организовать разными способами. Рассмотрим три основных способа:
F, описывающий управляемый процесс, содержит ссылку на управляющий объект - объект класса G. В свою очередь управляющий класс G содержит ссылку на управляемый объект - объект класса F.
F генерирует событие, а управляемый объект его обрабатывает. В свою очередь управляемый объект класса G генерирует событие, а управляющий объект его обрабатывает.
Что происходит, когда управляющий и управляемый процесс находятся в разных потоках? Все три способа взаимодействия осуществимы и в этом случае. Но когда управляющий процесс представлен интерфейсным классом, то возникает особенность, уже рассмотренная нами. Другим потокам запрещается напрямую обращаться к элементам управления интерфейсного класса, работающего в другом потоке.
Взаимодействие, построенное на основе механизма ссылок, управляющего интерфейсного класса и управляемого класса, реализующего бизнес - логику, подробно рассмотрено на примере проектов CorrectExample и WindowsInterfaceAndThreads. Справиться с проблемой доступа к элементам интерфейса из другого потока позволяет вызов метода Invoke, которым обладают все элементы интерфейса.
Недостаток схемы взаимодействия на основе ссылок состоит в том, что не только интерфейсный класс содержит ссылку на класс, реализующий бизнес - логику, но и класс бизнес - логики должен содержать ссылку на интерфейсный класс. Это противоречит правилам хорошего программирования, - бизнес - логика не должна привязываться к фиксированному интерфейсу. Схема, построенная на событиях, где объект, зажигающий событие, не знает, какие объекты и каких классов будут обрабатывать это событие, представляется предпочтительней. Но возможно ли зажигать событие в одном потоке, а обрабатывать его в другом потоке? Давайте рассмотрим подробнее эту схему.
Рассмотрим пример взаимодействия интерфейсного класса и класса бизнес - логики, основанное на событиях. Наша цель состоит в том, чтобы класс бизнес логики не содержал ссылку на интерфейсный класс. Интерфейсный класс, являющийся управляющим классом, конечно же, содержит ссылки на объекты, которыми он управляет. В тот момент, когда объект бизнес - логики вырабатывает новое значение параметра, принадлежащего к наблюдаемым параметрам, он будет, зажигая соответствующее событие, передавать это параметр всем объектам, принимающим событие. В интерфейсном классе, подписанном на получение сообщения о событии, событие может быть должным образом обработано.
Модифицируем наш последний пример. По аналогии с проектом WindowsInterfaceAndThreads построим проект WindowsFormsInterfaceAndEvents. Введем в рассмотрение класс Worker_Ev, который в отличие от ранее рассмотренного класса Worker, не будет содержать ссылку на интерфейсный класс, но будет включать событие, позволяющее уведомить интерфейсный класс о выработке новых значений наблюдаемых параметров.
Напомню, общую схему построения класса с событиями. Первым делом нужно объявить делегат, описывающий сигнатуру события. Принято, чтобы сигнатура события задавалась двумя параметрами, первый из которых задает объект, возбуждающий событие, а второй параметр представлял объект некоторого класса, наследуемого от класса EventArgs, содержащего параметры, передаваемые обработчикам события. В класс, вырабатывающий событие, нужно добавить объявление события с соответствующей сигнатурой, метод OnFire, зажигающий событие, и в нужных местах, где событие возникает, вызывать этот метод. В классах, которые хотят подписаться на получение сообщения о событии, следует подключить к событию обработчик события, имеющий соответствующую событию сигнатуру. Подробнее об этом можно прочесть в [9].
Вот как выглядит описание делегата, задающего сигнатуру события:
public delegate void ResultEventHandler(object sender, ResultEventArgs args);
Класс ResultEventArgs, описывающий параметры, передаваемые обработчикам события, выглядит так:
public class ResultEventArgs : EventArgs
{
/// <summary>
/// наблюдаемые параметры представляют
/// входные аргументы, передаваемые обработчику события
/// </summary>
string val, quality;
public string Val
{
get { return val; }
}
public string Quality
{
get { return quality; }
}
public ResultEventArgs(string val, string quality)
{
this.val = val;
this.quality = quality;
}
}
Класс Worker_Ev не содержит ссылки на интерфейсный класс, но содержит поле, задающее событие:
public event ResultEventHandler result_event;
Метод этого класса, которому обычно дается имя OnFire имеет стандартный вид:
/// <summary>
/// "Зажигает" события
/// </summary>
/// <param name="args">аргументы, передаваемые обработчику</param>
public void OnFire(ResultEventArgs args)
{
if(result_event != null)
result_event(this, args);
}
Метод, моделирующий процесс бизнес-логики, достаточно прост:
/// <summary>
/// Метод, осуществляющий процесс работы
/// </summary>
public void Run()
{
while (!stop)
{
SetVisionParams();
}
}
Переменная stop - это управляемая переменная. Когда в интерфейсном классе будет нажата соответствующая кнопка, выполнение цикла в Run завершится. Метод SetVisionParams вырабатывает значения наблюдаемых параметров, которые должны передаваться управляющему процессу через механизм событий. Вот текст этого метода:
/// <summary>
/// Создает значения наблюдаемых параметров
/// Зажигает событие
/// </summary>
public void SetVisionParams()
{
int val_i;
val_i = rnd.Next(minLimit, maxLimit + 1);
if (val_i == minLimit)
quality = EQuality.Ниже_Нормы.ToString();
else if (val_i == maxLimit)
quality = EQuality.Выше_нормы.ToString();
else
quality = EQuality.Норма.ToString();
numer++;
val = numer + "." + val_i;
quality = numer + "." + quality;
//зажигаем событие - получены результаты
OnFire(new ResultEventArgs(val, quality));
}
Интерфейсный класс, поле которого содержит ссылку worker, при запуске процесса бизнес - логики, создает этот объект и присоединяет к нему обработчик этого события:
private void buttonStart_Click(object sender, EventArgs e)
{
buttonClear.Enabled = false;
textBoxMessage.Text = "";
// Создаем объект бизнес-логики
worker = new Worker_Ev();
//Присоединяем обработчик события объекта worker
worker.result_event += new ResultEventHandler(SetVisions_Event);
//Устанавливаем границы
GetLimits();
//Создаем дочерний поток для выполения процесса бизнес-логики
Thread workerThread = new Thread(worker.Run);
//запускаем процесс
workerThread.Start();
}
Когда при работе процесса бизнес - логики в другом потоке, вырабатываются значения наблюдаемых параметров и зажигается событие, то в интерфейсном классе, получившем сообщение о событии, вызывается обработчик события:
/// <summary>
/// запись наблюдаемых параметров
/// в интерфейсные элементы
/// </summary>
public void SetVisions_Event(object sender, ResultEventArgs args)
{
listBoxValues.Items.Add(args.Val);
listBoxValues.Refresh();
listBoxQuality.Items.Add(args.Quality);
listBoxQuality.Refresh();
}
В данном обработчике события запись наблюдаемых параметров производится непосредственно в элементы интерфейса - элементы listBox. Все хорошо работает, если запускать Release версию приложения (Ctrl + F5). Но в Debug версии, при работе в отладочном режиме возникает исключительная ситуация, показанная на следующем рисунке:
(рис 8.3) Запрещенный доступ к элементам интерфейса из другого потока
Когда в приложении есть только два потока - один для интерфейса, другой для бизнес - логики, то работать без вызова метода Invoke безопасно. Но, конечно же, возникновение исключительной ситуации для отладочной версии не годится для профессиональной разработки. Необходимо найти другой способ работы в схеме событий.
Решается эта проблема достаточно просто. Записывать данные в элементы интерфейсного класса из другого потока не разрешается, но в поля этого класса запись разрешена. Поэтому вполне возможно создать в интерфейсном классе контейнер, куда будут записываться значения наблюдаемых параметров. Достаточно теперь в интерфейсном классе иметь командную кнопку, по нажатии которой на законных основаниях данные из контейнера будут переноситься в элементы интерфейса. Реализуем эту стратегию.
Обработчик события SetVisionsEvent, который приводил к исключительной ситуации, запишем теперь в следующем виде:
/// <summary>
/// запись наблюдаемых параметров
/// в контейнеры (списки)
/// </summary>
public void SetVisions_Event(object sender, ResultEventArgs args)
{
list_val.Add(args.Val);
list_quality.Add(args.Quality);
}
Здесь list_val и list_quality - два контейнера - два списка, в которые обработчик события помещает наблюдаемые параметры. В интерфейс проекта добавлена командная кнопка, позволяющая в нужный момент переносить данные из контейнеров в соответствующие элементы интерфейса, - списки, отображаемые на экране:
/// <summary>
/// перенос данных из контейнеров
/// в отображаемые элементы интерфейса
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void buttonDataSet_Click(object sender, EventArgs e)
{
for (int i = 0; i < list_val.Count; i++)
{
listBoxValues.Items.Add(list_val[i]);
listBoxQuality.Items.Add(list_quality[i]);
}
}
Заметьте, все эти изменения в интерфейсном классе, никак не отразились на классе Worker_Ev, генерирующем события.
Вот как выглядит теперь интерфейс нашего модифицированного проекта, обеспечивающего взаимодействие, основанное на событиях:
(рис 8.4) Интерфейс взаимодействия, основанного на событиях
Рассмотрим теперь более интересный проект, реализующий известную игру 15. Коротко об игре. Игра ведется на квадратном поле n * n. В классическом варианте n = 4. На этом поле в исходном состоянии размещается 15 квадратов, на каждом из которых написано одно из чисел - от 1 до 15. Порядок размещения квадратов случаен. Одно поле остается свободным. Цель игры в том, чтобы передвигая квадраты, используя одно свободное поле, упорядочить как можно больше квадратиков. В половине случаев удается правильно поставить все 15 квадратов. Но не для всякой начальной расстановки можно добиться этого результата. Гарантировать можно правильную расстановку лишь 13 квадратов. Математический анализ игры показал, что по начальной расстановке можно однозначно определить, достижимо ли полное упорядочение. Для этого достаточно подсчитать число нарушений порядка в исходной расстановке. Если оно нечетно, то правильно расставить можно лишь 13 квадратов.
Реализация этой игры представляет хорошее упражнение для программиста. На рис. 8.5 показан интерфейс проекта, реализующего игру 15:
(рис 8.5) Начальное состояние игры 15
Командная кнопка "Перемешать" позволяет задать новую случайную расстановку. Меню "Файл" позволяет сохранить расстановку или задать расстановку, записанную в файле. Меню "Игра" позволяет управлять свойствами игры, можно изменять скорость игры и менять игрока (человек или компьютер). Командные кнопки "Пуск" и "Останов" позволяют запустить или приостановить игру. Ряд элементов управления в интерфейсе позволяют отображать наблюдаемые параметры игры. В списке справа запоминается начальная расстановка, и записываются ходы, сделанные компьютером. Естественно, что каждый ход отображается в игровом поле. Еще два элемента сохраняют информацию о числе уже упорядоченных квадратов. Один элемент управления представляет текстовое поле, другой (ProgressBar) эту же информацию представляет в графическом виде. Еще одно поле служит для выдачи текстовых сообщений о ходе игры.
Игра представляет хороший образец на тему взаимодействия интерфейса и бизнес - логики. Здесь есть наблюдаемые параметры и управляемые параметры, которые можно менять по ходу игры (менять скорость, останавливать игру, продолжать выполнение, менять игрока).
Проект, реализующий эту игру, основан по схеме взаимодействия интерфейса и бизнес - логики, работающих в разных потоках, на основе взаимных ссылок. В данной ситуации это естественный вариант, поскольку игра рассчитана на вполне определенный интерфейс. При записи значений наблюдаемых параметров в соответствующие элементы интерфейса использовались методы Invoke.
Проект достаточно большой, поэтому приводить текст его не буду, но также как и все проекты, сопровождающие текст этого курса, как и сам курс, надеюсь, будет доступен на сайте Интернет университета ИТ (intuit.ru).
В завершении приведу снимок заключительного состояния игры, для случая, когда начальная расстановка позволяет правильно расставить все 15 квадратов:
(рис 8.6)
Подведем некоторые итоги. Все задачи, для решения которых мы пишем программы, можно условно разделить на пять классов:
Одной из целей учебного курса являлось рассмотрение широкого круга задач, позволяющее отнести задачу к тому или иному классу. При обосновании классификации можно использовать результаты анализа численных экспериментов, полученные при запуске программных проектов, создаваемых автором в ходе написания курса.
Программные проекты фактически являются частью курса, его дополнением, его поддержкой. Фрагменты проектов широко использовались в тексте курса. Тем не менее для программистов программный код не менее интересен, чем словесное описание. Исполняемому коду можно верить больше, чем словам. Поэтому все проекты, созданные автором для поддержки этого курса, с их кратким описанием будут доступны в интернете прежде всего на сайте открытого Интернет Университета ИТ – intuit.ru и на сайте НПО "Центрпрограммсистем" - cps.tver.ru
В качестве примера классификации рассмотрим задачу суммирования, широко обсуждаемую в учебном курсе. К какому из классов ее следует отнести? Для обоснования выводов был создан программный проект, а точнее, Решение (Solution) Sum, содержащее 5 проектов – DLL и 4 интерфейсных проекта.
В проектах рассматриваются три варианта вычисления суммы $$S= \sum_k a_k$$ , где $$a_k$$ это :
Начнем с задачи суммирования элементов массива. Вот результаты, полученные на моем компьютере (64-х битный компьютер с 6 Гб оперативной памяти и 4-мя физическими ядрами) при запуске Windows проекта из Решения Sum:
Как можно видеть, все алгоритмы – последовательный и параллельные - прекрасно справляются с задачей. Время суммирования вплоть до массива, содержащего десять миллионов элементов, составляет менее 0,1 секунды. В такой ситуации параллельные алгоритмы не имеют никакого преимущества перед последовательным алгоритмом. Так что задачу нахождения суммы элементов массива, также как нахождение максимального элемента и другие подобные ей задачи, следует отнести ко второму классу, где применение параллельных алгоритмов хотя и возможно, но нецелесообразно на компьютерах подобного класса.
Приведу теперь результаты экспериментов для задачи суммирования бесконечного сходящегося ряда на примере вычисления значений функции Arcsin(x). Эта задача интересна еще и тем, что эффективный последовательный алгоритм использует рекуррентное соотношение для вычисления очередного члена суммы. При распараллеливании такого эффективного приема не существует. Рекуррентное соотношение хотя и можно построить для параллельного шагового алгоритма, но оно значительно сложнее в сравнении с последовательным вариантом. Учитывая еще, что последовательный алгоритм практически мгновенно решает эту задачу, то у параллельного алгоритма нет шансов выиграть, что и подтверждается результатами экспериментов:
Можно видеть, что как последовательный, так и параллельный алгоритм прекрасно справляются с задачей. Время многократного (10000 повторов) вычисления значения функции составляет менее 0,1 секунды. Но опять-таки параллельный алгоритм не имеет никакого преимущества перед последовательным алгоритмом. Так что и эту задачу следует отнести ко второму классу, где применение параллельных алгоритмов хотя и возможно, но нецелесообразно на компьютерах подобного класса.
Полагаю, что справедлива следующая гипотеза:
Задачи с линейной временной сложностью относятся ко второму классу.
Для того чтобы параллельные алгоритмы выигрывали у последовательных алгоритмов исходная задача должно иметь по крайней мере квадратичную сложность. Суммирование конечного ряда, где каждый член ряда требует вычисления значения функции с линейной сложностью O(n), относится к подобным задачам. Вот как выглядят результаты эксперимента:
Рассматриваемую нами задачу, имеющую квадратичную временную сложность O(n2) следует отнести к третьему классу задач, допускающих эффективное распараллеливание.
Можно сделать и более общий вывод. Для потенциально распараллеливаемых задач со сложностью O(n2) и выше следует искать эффективные параллельные алгоритмы, позволяющие ощутимо сократить время решения задачи. Поиск таких алгоритмов не простая задача.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.