Опубликован: 23.04.2013 | Доступ: свободный | Студентов: 856 / 185 | Длительность: 12:54:00
Лекция 9:

Интерфейс и многопоточность

< Лекция 8 || Лекция 9: 12345 || Лекция 10 >

Три способа взаимодействия управляющего и управляемого процессов

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

  • Взаимодействие построено на основе механизма ссылок. Класс 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. Интерфейс взаимодействия, основанного на событиях
< Лекция 8 || Лекция 9: 12345 || Лекция 10 >
Алексей Рыжков
Алексей Рыжков

не хватает одного параметра:

static void Main(string[] args)
        {
            x = new int[n];
            Print(Sample1,"original");
            Print(Sample1P, "paralel");
            Console.Read();
        }

Никита Белов
Никита Белов

Выставил оценки курса и заданий, начал писать замечания. После нажатия кнопки "Enter" окно отзыва пропало, открыть его снова не могу. Кнопка "Удалить комментарий" в разделе "Мнения" не работает. Как мне отредактировать недописанный отзыв?