Создание Windows-приложений на основе Visual C#

Асинхронное программирование

Разбить на страницы
Показывать лекцию целиком
Для работы с данной лекцией используйте примеры.

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

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

Концепция асинхронного программирования

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

Локальные методы, вероятность исключений которых сведена к минимуму, не нуждаются в асинхронном подходе (например, изменение цвета шрифта текста или его размера), но другие методы (ожидание чтения файла или запуск web-службы) требуют его в самом начале разработки. Среда Common Language Runtime поддерживает достаточно широкое количество методов и классов асинхронного программирования.

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

(рис 7.1) Условная схема работы процессора

Когда мы запускаем "долгий" метод, процессор будет выполнять его, при этом ему будет "некогда" выполнять другие запросы пользователя (рис. 7.2).

(рис 7.2) Обработка методов процессором

При синхронном выполнении у приложения есть только один поток. С помощью асинхронной модели программирования вы можете запускать множество параллельных потоков и во время их выполнения реагировать на новые действия пользователя. После того как n-поток выполнен, вы отображаете результат на экран.

В обычной жизни есть масса примеров асинхронного поведения — пополнение запасов сырья на складе позволяет обеспечить фабрике бесперебойную работу. Несинхронной моделью будет полное использование сырья и последующий простой в ожидании подвоза материала.

Поддержка асинхронного программирования в .NET Framework

Классы, в которых есть встроенная поддержка асинхронной модели, имеют пару асинхронных методов для каждого из синхронных методов. Эти методы начинаются со слов Begin и End. Например, если мы хотим воспользоваться асинхронным вариантом метода Read класса System.IO.Stream, нам нужно использовать методы BeginRead и EndRead этого же класса.

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

Метод EndOperation

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

using System;
using System.IO;

namespace EndOperation
{

 class Class1
  {
	
	[STAThread]
	static void Main(string[] args)
	{	
		//Создаем поток и открываем файл. 
		FileStream fs = new FileStream("text.txt", FileMode.Open);
		byte[] fileBytes = new byte[fs.Length];
		// Запуск метода Read в параллельном потоке.
		IAsyncResult ar = fs.BeginRead(fileBytes, 0, fileBytes.Length, null, null);
		for(int i = 0; i<10000000; i++)
		{
			// Имитация длительной работы основного
			// потока, не зависящая от выполнения асинхронного метода.
		}
		// После завершения работы основного потока
		// запускаем завершение выполнения параллельного 
		// метода Read.
		fs.EndRead(ar);
		Console.Write(System.Text.Encoding.Default.GetString(fileBytes));
	}
 }
}

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

Способ опроса (Pooling)

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

using System;
using System.IO;

namespace Pooling
{	
 class Class1
  {		
	[STAThread]
	static void Main(string[] args)
	{
		FileStream fs = new FileStream("text.txt", FileMode.Open);
		byte[] fileBytes = new byte[fs.Length];
		Console.Write("Выполнение метода Read асинхронно.");
		// Запуск метода Read асинхронно. 
		IAsyncResult ar = fs.BeginRead(fileBytes, 0, fileBytes.Length, null, null);			
		// Проверка на выполнение асинхронного метода.
		while(!ar.IsCompleted)
		{
			// Во время чтения файла на экран выводится надпись
			Console.Write("Процесс идет");
		}
		Console.WriteLine();
		string textFromFile = System.Text.Encoding.Default.GetString(fileBytes);
		Console.Write(textFromFile);
	}
 }
}

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

Способ Callback

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

Пример использования варианта Callback:

using System;
using System.IO;

namespace Callback
{
	
	class Class1
	{		
		// Создаем поток и массив байтов.
		static FileStream fs;
		static byte[] fileBytes;
		[STAThread]
		static void Main(string[] args)
		{			
			// Открываем файл в поток.
			fs = new FileStream("text.txt", FileMode.Open);
			fileBytes = new byte[fs.Length];
			// Запускаем  метод Read асинхронно с передачей в качестве Сallback
			// метода WorkComplete
			fs.BeginRead(fileBytes, 0, (int)fs.Length, new AsyncCallback(WorkComplete), null);
		}
		/// <summary>
		/// Метод, вызываемый автоматически при завершении работы параллельного потока.
		/// </summary>
		/// <param name="ar">Объект типа IAsyncResult.</param>
		static void WorkComplete(IAsyncResult ar)
		{
			// Запуск окончания метода.
			fs.EndRead(ar);
			string textFromFile = System.Text.Encoding.Default.GetString(fileBytes);
			Console.Write(textFromFile);
		}
	}
}

На диске, прилагаемом к книге, вы найдете приложения EndOperation, Pooling и Callback (Code\Glava7\ EndOperation, Pooling, Callback).

Асинхронный запуск произвольного метода

При разработке программного обеспечения наиболее часто требуется запускать асинхронно собственные методы. Для этого нужно проделать следующие шаги:

  • Создать и запустить делегат с необходимой сигнатурой. После этого можно работать со своим методом так же, как и с методами со встроенной поддержкой асинхронной модели программирования.
  • Выбрать механизм оповещения о завершении и подготовить для него все необходимое.
  • Запустить метод асинхронно.
  • Получить результаты в основном потоке и обновить пользовательский интерфейс.
  • Создайте новое Windows-приложение и назовите его ArbitraryMethod. Добавляем на форму три надписи, два текстовых поля и одну кнопку, и устанавливаем им следующие свойства:

    button1, свойствоЗначение
    Name BtnRun
    Location 16; 64
    Text Сумма
    label1, свойствоЗначение
    Name LblA
    Location 8; 24
    Size 72; 23
    Text Значение А
    label2, свойствоЗначение
    Name LblB
    Location 216; 24
    Size 72; 23
    Text Значение В
    label3, свойствоЗначение
    Name LblResult
    Location 128; 64
    Size 272; 23
    Text
    textBox1, свойствоЗначение
    Name TxbA
    Location 88; 24
    Text
    textBox2, свойствоЗначение
    Name TxbB
    Location 296; 24
    Text

    Устанавливаем свойству Size формы значение 424; 144 и заголовок "Асинхронный запуск произвольного метода". Переходим в код формы. Создание делегата в данном случае ничем не отличается от общих правил, рассмотренных в первой лекции:

    // Делегат, принимающий две переменные типа int и возвращающий одну переменную типа int,
    // для асинхронного запуска метода (Summ)
    
    private delegate int AsyncSumm(int a, int b);

    Создаем метод Summ, в котором будут складываться числа, вводимые в два текстовых поля:

    private int Summ(int a, int b)
    	{
    		return a+b;
    	}

    Обработчик кнопки btnRun будет выглядеть следующим образом:

    private void btnRun_Click(object sender, System.EventArgs e)
    	{
    		int a, b;
    		try
    		{
    			// Преобразование типов данных.
    			a = Int32.Parse(txbA.Text);
    			b = Int32.Parse(txbB.Text);
    		}
    		catch(Exception)
    		{
    			MessageBox.Show("При выполнении преобразования типов возникла ошибка");
    			txbA.Text = txbB.Text = "";
    			return;
    		}
    		AsyncSumm summdelegate = new AsyncSumm(Summ);
    		AsyncCallback cb = new AsyncCallback(CallBackMethod);
    		summdelegate.BeginInvoke(a, b, cb, summdelegate);
    	}

    Мы создали экземпляр summdelegate делегата AsyncSumm и проинициализировали его методом Summ:

    AsyncSumm summdelegate = new AsyncSumm(Summ);

    Для использования механизма Сallback создали экземпляр делегата AsyncCallBack:

    AsyncCallback cb = new AsyncCallback(CallBackMethod);

    Метод CallBackMethod имеет сигнатуру, принимающую один параметр типа IAsyncResult и не возвращающую значений. После того как делегат инициализирован методом, можно запускать прикрепленный к делегату метод асинхронно с помощью метода BeginInvoke. Этот метод принимает две переменные типа int а и b, экземпляр cb делегата AsyncCallback и экземпляр summdelegate делегата SummDelegate:

    summdelegate.BeginInvoke(a, b, cb, summdelegate);

    Создаем метод CallBackMethod, который привязан к делагату summdelegate:

    private void CallBackMethod(IAsyncResult ar)
     {
       AsyncSumm summdelegate = (AsyncSumm)ar.AsyncState;
       lblResult.Text = String.Format("Сумма введенных чисел равна {0}", summdelegate.EndInvoke(ar));
     }

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

    При запуске приложения после ввода чисел и нажатия кнопки "Сумма" метод EndInvoke возвращает результат в надпись (рис. 7.3).

    (рис 7.3) Приложение ArbitraryMethod

    На диске, прилагаемом к книге, вы найдете приложение ArbitraryMethod (Code\Glava7\ ArbitraryMethod).

    Асинхронный запуск метода с различными вариантами завершения вызова

    Рассмотрим теперь асинхронный запуск метода и его завершение несколькими способами в очень простом Windows-приложении. Создайте новое приложение и назовите его AsyncMethods. Добавляем на форму три кнопки и три надписи. Устанавливаем следующие свойства элементов управления:

    button1, свойствоЗначение
    Name btnEnd
    Location 8; 16
    Text End
    button2, свойствоЗначение
    Name btnIsComplete
    Location 8; 56
    Text IsComplete
    button3, свойствоЗначение
    Name btnCallback
    Location 8; 96
    Text Callback
    label1, свойствоЗначение
    Name lblEnd
    Location 104; 16
    Size 168; 23
    Text
    label2, свойствоЗначение
    Name lblIsComplete
    Location 104; 56
    Size 168; 23
    Text
    label3, свойствоЗначение
    Name lblCallback
    Location 104; 96
    Size 168; 23
    Text

    Переходим в код формы. Создаем делегат AsynchronousMethodDelegate:

    private delegate string AsynchronousMethodDelegate(ActivateType source);

    Создаем метод, предназначенный для асинхронного запуска:

    private string AsynchronousMethod(ActivateType source)
    	{
    		string result = String.Empty;
    		switch(source)
    		{
    			case ActivateType.Callback:
    				result = "Callback";
    				break;
    			case ActivateType.End:
    				result = "End";
    				break;
    			case ActivateType.IsComplete:
    				result = "Is Complete";
    				break;
    		}
    		return result;
    	}

    В зависимости от выбора значения перечисления ActivateType метод будет возвращать одну из трех переменных типа stringCallback, End или Is Complete. Выбранное значение перечисления будет типом завершения вызова:

    private enum ActivateType
    	{
    		End,
    		IsComplete,
    		Callback
    	}

    В обработчике кнопки btnEnd выводим результат работы асинхронного метода на надпись lblEnd:

    private void btnEnd_Click(object sender, System.EventArgs e)
    	{
    		AsynchronousMethodDelegate asyncDeleg = new 
    			AsynchronousMethodDelegate(AsynchronousMethod);
    		// Запускаем асинхронно метод AsynchronousMethod.
    		IAsyncResult ar = asyncDeleg.BeginInvoke(ActivateType.End, null, null);
    		// Получаем  результаты асинхронного метода.
    		lblEnd.Text = asyncDeleg.EndInvoke(ar);
    	}

    В обработчике кнопки btnIsComplete применяем способ завершения работы Pooling и также выводим результат на надпись lblIsComplete:

    private void btnIsComplete_Click(object sender, System.EventArgs e)
    	{
    		AsynchronousMethodDelegate asyncDeleg = new 
    			AsynchronousMethodDelegate(AsynchronousMethod);
    	// Запускаем асинхронно метод AsynchronousMethod.
    		IAsyncResult ar = asyncDeleg.BeginInvoke(ActivateType.IsComplete, null, null);
    	while(!ar.IsCompleted)
    		{
    		lblIsComplete.Text = "Ожидание";
    		}
    		// Получаем  результаты асинхронного метода.
    		lblIsComplete.Text = asyncDeleg.EndInvoke(ar);
    	}

    В обработчике кнопки применяем способ завершения работы Callback:

    private void btnCallback_Click(object sender, System.EventArgs e)
    	{
    		AsynchronousMethodDelegate asyncDeleg = new 
    			AsynchronousMethodDelegate(AsynchronousMethod);
    		// Создаем экземпляр callback  делегата Callback.
    		AsyncCallback callback = new AsyncCallback(CallbackMethod);
    			// Запускаем асинхронно метод AsynchronousMethod.
    		asyncDeleg.BeginInvoke(ActivateType.Callback, callback, asyncDeleg);
    	}

    Добавляем метод для завершения вызова и выводим результат на надпись lblCallback:

    private void CallbackMethod(IAsyncResult ar)
    	{
    		// Приводим  к типу AsynchronousMethodDelegate
    AsynchronousMethodDelegate asyncDeleg = (AsynchronousMethodDelegate)ar.AsyncState;
    			// Получаем  результаты асинхронного метода.
    		lblCallback.Text = asyncDeleg.EndInvoke(ar);
    	}

    Запускаем приложение. Его работа предельно проста — при нажатии на кнопки запускается метод AsynchronousMethod, который завершается одним из асинхронных способов (рис. 7.4).

    (рис 7.4) Приложение AsyncMethods

    На диске, прилагаемом к книге, вы найдете приложение AsyncMethods (Code\Glava7\AsyncMethods).

    Обновление пользовательского интерфейса в Windows-приложениях

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

    Создайте новое Windows-приложение и назовите его TestMethodInvoker. Перетаскиваем на форму три кнопки (точное расположение их не важно) и свойству Name устанавливаем значения btnAddSync, btnAsync, btnAddAsyncMI (в соответствии со свойством Text на рис. 7.5).

    (рис 7.5) Приложение TestMethodInvoker в режиме дизайна

    В Windows-приложениях перед обновлением пользовательского интерфейса необходимо вернуть элемент управления в основной поток. Это делается с помощью делегата MethodInvoker, который не принимает и не возвращает параметров.

    Чтобы инициализировать MethodInvoker-делегат, нужно создать метод, обновляющий пользовательский интерфейс:

    // Метод, обновляющий пользовательский интерфейс
    private void UpdateUI()	
    {}
    // Инициализация делегата MethodInvoker
    MethodInvoker methodinvoker = new MethodInvoker(UpdateUI);

    Для запуска экземпляра methodinvoker делегата MethodInvoker асинхронно — вызываем метод BeginInvoke текущей формы:

    // Асинхронный запуск 
    this.BeginInvoke(methodinvoker);

    Добавляем обработчиков кнопок:

    private void btnAddSync_Click(object sender, System.EventArgs e)
    {
    // Синхронный запуск метода UpdateUI
    UpdateUI();		
    }
    	
    // Делегат для вызова метода UpdateUI асинхронно.
    delegate void AddItemAsyncDelegate();
    
    private void btnAddAsyncMI_Click(object sender, System.EventArgs e)
    {
    // Асинхронный вызов метода UpdateAsync
    AddItemAsyncDelegate asyncDel = new AddItemAsyncDelegate(UpdateAsync);
    asyncDel.BeginInvoke(null, null);
    }
    private void UpdateAsync()
    {
    // Асинхронный вызов метода UpdateUI
    MethodInvoker methodinvoker = new MethodInvoker(UpdateUI);
    this.BeginInvoke(methodinvoker);
    }
    private void UpdateUI()
    {
    // Добавление элемента управления.
    Button btn = new Button();
    btn.Text = "Кнопка";
    this.Controls.Add(btn);
    }
    		
    
    private void btnAsync_Click(object sender, System.EventArgs e)
    {
    // Асинхронный вызов метода UpdateUI
    // При выполнении этого метода элемент управления на форму 
    // не добавится, поскольку он создан не в основном потоке.
    AddItemAsyncDelegate asyncDel = new AddItemAsyncDelegate(UpdateUI);
    asyncDel.BeginInvoke(null, null);
    }

    Запускаем приложение. При нажатии кнопки Sync кнопка появляется самым обычным способом — в ее обработчике просто вызывается метод UpdateUI. В обработчиках кнопок Async и Async with MI вызывается метод UpdateUI, только в первом случае элемент создается не в основном потоке, поэтому не происходит обновления пользовательского интерфейса, а во втором случае элемент возвращается в основной поток – и кнопка появляется на форме.

    Использование делегата MethodInvoker необходимо только в Windows-приложениях.

    На диске, прилагаемом к книге, вы найдете приложение TestMethodInvoker (Code\Glava7\ TestMethodInvoker).

    Меры безопасности в многопоточных приложениях

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

    Самые распространенные проблемы, возникающие при использовании многопоточной модели программирования, — это "Гонка" (Race condition) и "Мертвая точка" (Deadlock).

    Проблема "Гонки" встречается, когда два или более потоков начинают использовать одновременно один объект. Например, в то время как один поток считывает данные из структуры, другой может начать изменять ее. После выполнения этих операций неизвестно, какие данные считал первый поток: старые, новые или перемешанные.

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

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

    Одним из решений этих проблем является закрытие общего доступа к объектам между потоками. Но иногда приходится использовать одни переменные в многопоточных приложениях. Для таких целей в пространстве имен System.Threading находятся классы, помогающие осуществить синхронизацию объектов.

    Автоматическая синхронизация

    Библиотека .NET Framework предоставляет автоматическую синхронизацию объектов. Для этого служит атрибут SynchronizationAttribute, находящийся в пространстве имен System.Runtime.Remoting.Contexts. Его можно установить на любой класс, унаследованный от класса ContextBoundObject. При этом все свойства и методы этого класса будут синхронизированы, т.е. только один поток сможет работать с объектом этого класса. Для использования объектов внутри этого класса несколькими потоками следует их сделать статическими, т. е. установить свойство static. Пример использования этого атрибута:

    using System;
    using System.Runtime.Remoting.Contexts;
    
    namespace AutomaticSynchronization
    {	
    	[Synchronization()]
    	class MyClass:ContextBoundObject
    	{
    		string myName;
    		/// <summary>
    		/// Конструктор
    		/// </summary>
    		/// <param name="name">Имя.</param>
    		public MyClass(string name)
    		{
    			this.myName = name;
    		}
    		/// <summary>
    		/// Параллельные потоки НЕ могут 
    		/// одновременно использовать свойство Name
    		/// </summary>
    		public string Name
    		{
    			get { return myName;}
    			set { myName = value;}
    		}
    		static int myAge;
    		/// <summary>
    		/// Статический конструктор.
    		/// </summary>
    		static MyClass()
    		{
    			myAge = 15;
    		}
    		/// <summary>
    		/// Возраст. Статическое свойство. Параллельные потоки могут 
    		/// одновременно использовать это свойство.
    		/// </summary>
    		public static int Age
    		{
    			get{ return myAge;}
    			set{ myAge = value;}
    		}
    	}
    }

    Использование областей синхронизации

    Следующим способом синхронизации является использование ключевого слова lock. Его использует класс Monitor из пространства имен System.Threading для блокировки области кода. Это ключевое слово может применяться и в статических методах. Оно имеет следующий синтаксис:

    lock(объект) блок выражений.

    Пример использования ключевого слова lock:

    public void Function()
    {
        System.Object lockThis = new System.Object();
        lock(lockThis)
        {
    // Код, который нуждается в эксклюзивных правах
        }
    }

    В процессе компиляции приложения среда заменяет выражение lock на блок try, finally:

    SomeClass x = new SomeClass();
    lock(x)
    {
        // Выполнение действий.
        DoSomething();
    }
    
       	// Эквивалентный код:
    
    SomeClass x = new SomeClass();
    System.Object obj = (System.Object)x;
    System.Threading.Monitor.Enter(obj);
    try
    {
        // Выполнение действий.
        DoSomething();
    }
    finally
    {
        System.Threading.Monitor.Exit(obj);
    }

    В статических методах lock можно включать прямо внутри свойств:

    using System;
    using System.Threading;
    
    class Cache
    {
    public static void Add(object x)
    {
    // Код, который не нуждается в эксклюзивных правах
    lock (typeof(Cache))
    {
    // Код, который нуждается в эксклюзивных правах
    }
    // Код, который не нуждается в эксклюзивных правах
    }
    }

    Ручная синхронизация

    Библиотека .NET Framework предлагает еще один способ синхронизации — применение атрибута MethodImplAttribute, принадлежащего пространству имен System.Runtime.CompilerServices. В отличие от ключевого слова lock или класса Monitor, действие этого атрибута распространяется не на отдельный кусок кода, а на весь метод. Его можно использовать как с методами объекта, так и со статическими методами. Например, класс SomeClass имеет два метода с синхронизацией доступа, один из который статический:

    using System;
    using System.Threading;
    using System.Runtime.CompilerServices;
    
    namespace Sample
    {
    	
    	/// <summary>
    	/// Применение  атрибута MethodImplAttribute
    	/// </summary>
    	public class SomeClass
    	{
    		/// <summary>
    		/// Статическая переменная.
    		/// </summary>
    		static int staticValue = 0;
    		/// <summary>
    		/// Переменная экземпляра.
    		/// </summary>
    		int instanceValue = 0;
    		/// <summary>
    		/// Синхронизированный доступ к переменной экземпляра.
    		/// </summary>
    		[MethodImplAttribute(MethodImplOptions.Synchronized)]
    		public void ChangeInstance()
    		{
    			// Действия метода.
    		}
    		/// <summary>
    		/// Синхронизированный доступ к статической переменной
    		/// </summary>
    		[MethodImplAttribute(MethodImplOptions.Synchronized)]
    		public static void ChangeStatic()
    		{
    			// Действия метода.
    		}
    	}
    }

    В приведенном примере с web-службой используется техника callback для завершения вызова метода HelloWorld. Обратите внимание, что web-служба автоматически создает асинхронные варианты всех методов, помеченных атрибутом [WebMethod].

    Подключение к web-сервису

    Web-сервисы представляют собой отдельные компоненты, к которым можно получить доступ через Интернет и задействовать их в своем web-приложении. Их использование напоминает подключение компонент .NET или СOM в Windows-приложениях и зачастую существенно облегчает разработку. Более подробную информацию о web-сервисах вы сможете найти в литературе, посвященной технологии ASP.NET.

    Подключение к web-сервисам — это, как правило, длительный процесс, занимающий несколько секунд. Рассмотрим два варианта подключения к сервису: один будет подключаться к web-службе синхронно, другой — асинхронно.

    Создадим вначале web-сервис. В Visual Studio .NET создаем новый проект, в окне шаблонов выбираем ASP.NET Web Service и называем его HelloWorldService (рис. 7.6).

    (рис 7.6) Создание web-сервиса

    Переключаемся в режим кода и убираем комментарии с существующего метода HelloWorld (рис. 7.7).

    (рис 7.7) Режим кода HelloWorldService

    Добавляем в тело метода следующий код:

    [WebMethod]
    	public string HelloWorld()
    	{
    		// Имитация длительной работы метода
    		System.Threading.Thread.Sleep(2000);
    		return "Hello World";
    }

    Компилируем проект и закрываем его. Теперь займемся Windows-приложением, которое будет вызывать Web-Service. Создайте новый проект и назовите его WebService. В окне Solution Explorer щелкаем правой кнопкой на папке References и в появившемся меню выбираем пункт Add Web Reference (рис. 7.8).

    (рис 7.8) Приложение WebService в режиме дизайна. Добавление ссылки на web-сервис

    В появившемся окне нажимаем на ссылку Web Services on the local machine (рис. 7.9).

    (рис 7.9) Окно Add Web Reference

    Из появившегося списка выбираем только что созданный web-сервис и переходим по ссылке (рис. 7.10).

    (рис 7.10)

    В поле Web Reference Name вводим название WebService и нажимаем Add Reference. Название web-cервиса будет названием proxy-класса (рис. 7.11).

    (рис 7.11) Свойства выбранного сервиса

    После добавления сервиса к приложению в окне Solution Explorer появляется ссылка на него.

    Из окна Toolbox перетаскиваем на форму две кнопки и надпись. Расположите их так, как показано на рис. 7.7. Называем кнопки btnSync и btnAsync соответственно, а надпись — lblHelloWorld. В обработчике кнопки btnSync будем вызывать сервис синхронно, т. е. обычным способом:

    private void btnSync_Click(object sender, System.EventArgs e)
    	{
    		// Деактивируем кнопку.
    		btnSync.Enabled = false;
    		// Создаем  экземпляр Web-службы
    		WebService.Service1 service = new 
    			WebService.Service1();
    		// Запуск метода
    		lblHelloWorld.Text = service.HelloWorld();
    		// Активируем  кнопку.
    		btnSync.Enabled = true;
    
    	}

    Для асинхронного запуска добавляем обработчик кнопки btnAsync и метод AsyncCallBackMethod:

    private void btnAsync_Click(object sender, System.EventArgs e)
    	{
    		// Деактивируем кнопку.
    		btnAsync.Enabled = false;
    		// Создаем  экземпляр web-службы
    		WebService.Service1 service = new WebService.Service1();			
    		// Запуск метод асинхронно
    		service.BeginHelloWorld(new AsyncCallback(AsyncCallBackMethod), service);
    
    	}
    	private void AsyncCallBackMethod(IAsyncResult ar)
    	{
    		WebService.Service1 service = (WebService.Service1)ar.AsyncState;
    		lblHelloWorld.Text = service.EndHelloWorld(ar);
    		// Активируем  кнопку.
    		btnAsync.Enabled = true;
    	}

    Запускаем проект и нажимаем на кнопку Sync. Кнопка становится неактивной на время подключения к web-сервису, затем выводится строка Hello World — содержимое метода HelloWorld. В течение подключения приложение зависает; попробуйте перемещать форму по экрану. Нажимаем на кнопку btnAsync — в этом случае форма отвечает на запросы, поскольку основной поток свободен, а выполнение метода web-службы происходит в параллельном потоке.

    На диске, прилагаемом к книге, вы найдете приложение WebService (Code\Glava7\ WebService).

    Элемент управления ProgressBar

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

    button1, свойствоЗначение
    Name btnEnter
    Location 128; 216
    Text Ввод
    button2, свойствоЗначение
    Name btnStart
    Location 208; 216
    Text Пуск
    textBox1, свойствоЗначение
    Location 16; 216
    Text
    progressBar1, свойствоЗначение
    Location 16; 32
    Maximum 30000
    Size 264; 23
    pictureBox1, свойствоЗначение
    Image Code\Glava7\ProgressBar\Image\FILECOPY.gif
    Maximum 30000
    Location 16; 88
    Size 264; 72
    Visible False

    Элемент ProgressBar имеет свойство Value, принимающее численное значение, которое отображается на шкале заполнения. Мы установили максимальное значение этой шкалы 30000. При нажатии на кнопку "Ввод" значение, введенное в текстовое поле, будет устанавливаться на шкале:

    private void btnEnter_Click(object sender, System.EventArgs e)
     {
    
       try
       {
    	   progressBar1.Value = Convert.ToInt32(textBox1.Text);
    				
    				
       }
    	 catch(Exception)
      {
    	MessageBox.Show("При выполнении преобразования типов возникла ошибка");
    	return;
    	}
     }

    При нажатии на кнопку "Пуск" будет запускаться простой процесс, который будет отражать ProgressBar, а также появится стандартный рисунок копирования:

    private void btnStart_Click(object sender, System.EventArgs e)
     {
       pictureBox1.Visible = true;
       for(int i =0; i<30000; i++)
       {
    	  progressBar1.Value = i;
       }
     }

    Запускаем приложение (рис. 7.12). Процесс выполняется в синхронном режиме — попробуйте перемещать форму в момент запуска.

    (рис 7.12) Приложение ProgressBar

    Даже такое простое приложение требуется запускать в асинхронном режиме. Переделайте его самостоятельно, используя в качестве примера приложение ArbitraryMethod.

    На диске, прилагаемом к книге, вы найдете приложение ProgressBar (Code\Glava7\ ProgressBar).

    Страницы:
    Для работы с данной лекцией используйте примеры.

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

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

    Концепция асинхронного программирования

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

    Локальные методы, вероятность исключений которых сведена к минимуму, не нуждаются в асинхронном подходе (например, изменение цвета шрифта текста или его размера), но другие методы (ожидание чтения файла или запуск web-службы) требуют его в самом начале разработки. Среда Common Language Runtime поддерживает достаточно широкое количество методов и классов асинхронного программирования.

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

    (рис 7.1) Условная схема работы процессора

    Когда мы запускаем "долгий" метод, процессор будет выполнять его, при этом ему будет "некогда" выполнять другие запросы пользователя (рис. 7.2).

    (рис 7.2) Обработка методов процессором

    При синхронном выполнении у приложения есть только один поток. С помощью асинхронной модели программирования вы можете запускать множество параллельных потоков и во время их выполнения реагировать на новые действия пользователя. После того как n-поток выполнен, вы отображаете результат на экран.

    В обычной жизни есть масса примеров асинхронного поведения — пополнение запасов сырья на складе позволяет обеспечить фабрике бесперебойную работу. Несинхронной моделью будет полное использование сырья и последующий простой в ожидании подвоза материала.

    Поддержка асинхронного программирования в .NET Framework

    Классы, в которых есть встроенная поддержка асинхронной модели, имеют пару асинхронных методов для каждого из синхронных методов. Эти методы начинаются со слов Begin и End. Например, если мы хотим воспользоваться асинхронным вариантом метода Read класса System.IO.Stream, нам нужно использовать методы BeginRead и EndRead этого же класса.

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

    Метод EndOperation

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

    using System;
    using System.IO;
    
    namespace EndOperation
    {
    
     class Class1
      {
    	
    	[STAThread]
    	static void Main(string[] args)
    	{	
    		//Создаем поток и открываем файл. 
    		FileStream fs = new FileStream("text.txt", FileMode.Open);
    		byte[] fileBytes = new byte[fs.Length];
    		// Запуск метода Read в параллельном потоке.
    		IAsyncResult ar = fs.BeginRead(fileBytes, 0, fileBytes.Length, null, null);
    		for(int i = 0; i<10000000; i++)
    		{
    			// Имитация длительной работы основного
    			// потока, не зависящая от выполнения асинхронного метода.
    		}
    		// После завершения работы основного потока
    		// запускаем завершение выполнения параллельного 
    		// метода Read.
    		fs.EndRead(ar);
    		Console.Write(System.Text.Encoding.Default.GetString(fileBytes));
    	}
     }
    }

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

    Способ опроса (Pooling)

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

    using System;
    using System.IO;
    
    namespace Pooling
    {	
     class Class1
      {		
    	[STAThread]
    	static void Main(string[] args)
    	{
    		FileStream fs = new FileStream("text.txt", FileMode.Open);
    		byte[] fileBytes = new byte[fs.Length];
    		Console.Write("Выполнение метода Read асинхронно.");
    		// Запуск метода Read асинхронно. 
    		IAsyncResult ar = fs.BeginRead(fileBytes, 0, fileBytes.Length, null, null);			
    		// Проверка на выполнение асинхронного метода.
    		while(!ar.IsCompleted)
    		{
    			// Во время чтения файла на экран выводится надпись
    			Console.Write("Процесс идет");
    		}
    		Console.WriteLine();
    		string textFromFile = System.Text.Encoding.Default.GetString(fileBytes);
    		Console.Write(textFromFile);
    	}
     }
    }

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

    Способ Callback

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

    Пример использования варианта Callback:

    using System;
    using System.IO;
    
    namespace Callback
    {
    	
    	class Class1
    	{		
    		// Создаем поток и массив байтов.
    		static FileStream fs;
    		static byte[] fileBytes;
    		[STAThread]
    		static void Main(string[] args)
    		{			
    			// Открываем файл в поток.
    			fs = new FileStream("text.txt", FileMode.Open);
    			fileBytes = new byte[fs.Length];
    			// Запускаем  метод Read асинхронно с передачей в качестве Сallback
    			// метода WorkComplete
    			fs.BeginRead(fileBytes, 0, (int)fs.Length, new AsyncCallback(WorkComplete), null);
    		}
    		/// <summary>
    		/// Метод, вызываемый автоматически при завершении работы параллельного потока.
    		/// </summary>
    		/// <param name="ar">Объект типа IAsyncResult.</param>
    		static void WorkComplete(IAsyncResult ar)
    		{
    			// Запуск окончания метода.
    			fs.EndRead(ar);
    			string textFromFile = System.Text.Encoding.Default.GetString(fileBytes);
    			Console.Write(textFromFile);
    		}
    	}
    }

    На диске, прилагаемом к книге, вы найдете приложения EndOperation, Pooling и Callback (Code\Glava7\ EndOperation, Pooling, Callback).

    Асинхронный запуск произвольного метода

    При разработке программного обеспечения наиболее часто требуется запускать асинхронно собственные методы. Для этого нужно проделать следующие шаги:

  • Создать и запустить делегат с необходимой сигнатурой. После этого можно работать со своим методом так же, как и с методами со встроенной поддержкой асинхронной модели программирования.
  • Выбрать механизм оповещения о завершении и подготовить для него все необходимое.
  • Запустить метод асинхронно.
  • Получить результаты в основном потоке и обновить пользовательский интерфейс.
  • Создайте новое Windows-приложение и назовите его ArbitraryMethod. Добавляем на форму три надписи, два текстовых поля и одну кнопку, и устанавливаем им следующие свойства:

    button1, свойствоЗначение
    Name BtnRun
    Location 16; 64
    Text Сумма
    label1, свойствоЗначение
    Name LblA
    Location 8; 24
    Size 72; 23
    Text Значение А
    label2, свойствоЗначение
    Name LblB
    Location 216; 24
    Size 72; 23
    Text Значение В
    label3, свойствоЗначение
    Name LblResult
    Location 128; 64
    Size 272; 23
    Text
    textBox1, свойствоЗначение
    Name TxbA
    Location 88; 24
    Text
    textBox2, свойствоЗначение
    Name TxbB
    Location 296; 24
    Text

    Устанавливаем свойству Size формы значение 424; 144 и заголовок "Асинхронный запуск произвольного метода". Переходим в код формы. Создание делегата в данном случае ничем не отличается от общих правил, рассмотренных в первой лекции:

    // Делегат, принимающий две переменные типа int и возвращающий одну переменную типа int,
    // для асинхронного запуска метода (Summ)
    
    private delegate int AsyncSumm(int a, int b);

    Создаем метод Summ, в котором будут складываться числа, вводимые в два текстовых поля:

    private int Summ(int a, int b)
    	{
    		return a+b;
    	}

    Обработчик кнопки btnRun будет выглядеть следующим образом:

    private void btnRun_Click(object sender, System.EventArgs e)
    	{
    		int a, b;
    		try
    		{
    			// Преобразование типов данных.
    			a = Int32.Parse(txbA.Text);
    			b = Int32.Parse(txbB.Text);
    		}
    		catch(Exception)
    		{
    			MessageBox.Show("При выполнении преобразования типов возникла ошибка");
    			txbA.Text = txbB.Text = "";
    			return;
    		}
    		AsyncSumm summdelegate = new AsyncSumm(Summ);
    		AsyncCallback cb = new AsyncCallback(CallBackMethod);
    		summdelegate.BeginInvoke(a, b, cb, summdelegate);
    	}

    Мы создали экземпляр summdelegate делегата AsyncSumm и проинициализировали его методом Summ:

    AsyncSumm summdelegate = new AsyncSumm(Summ);

    Для использования механизма Сallback создали экземпляр делегата AsyncCallBack:

    AsyncCallback cb = new AsyncCallback(CallBackMethod);

    Метод CallBackMethod имеет сигнатуру, принимающую один параметр типа IAsyncResult и не возвращающую значений. После того как делегат инициализирован методом, можно запускать прикрепленный к делегату метод асинхронно с помощью метода BeginInvoke. Этот метод принимает две переменные типа int а и b, экземпляр cb делегата AsyncCallback и экземпляр summdelegate делегата SummDelegate:

    summdelegate.BeginInvoke(a, b, cb, summdelegate);

    Создаем метод CallBackMethod, который привязан к делагату summdelegate:

    private void CallBackMethod(IAsyncResult ar)
     {
       AsyncSumm summdelegate = (AsyncSumm)ar.AsyncState;
       lblResult.Text = String.Format("Сумма введенных чисел равна {0}", summdelegate.EndInvoke(ar));
     }

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

    При запуске приложения после ввода чисел и нажатия кнопки "Сумма" метод EndInvoke возвращает результат в надпись (рис. 7.3).

    (рис 7.3) Приложение ArbitraryMethod

    На диске, прилагаемом к книге, вы найдете приложение ArbitraryMethod (Code\Glava7\ ArbitraryMethod).

    Асинхронный запуск метода с различными вариантами завершения вызова

    Рассмотрим теперь асинхронный запуск метода и его завершение несколькими способами в очень простом Windows-приложении. Создайте новое приложение и назовите его AsyncMethods. Добавляем на форму три кнопки и три надписи. Устанавливаем следующие свойства элементов управления:

    button1, свойствоЗначение
    Name btnEnd
    Location 8; 16
    Text End
    button2, свойствоЗначение
    Name btnIsComplete
    Location 8; 56
    Text IsComplete
    button3, свойствоЗначение
    Name btnCallback
    Location 8; 96
    Text Callback
    label1, свойствоЗначение
    Name lblEnd
    Location 104; 16
    Size 168; 23
    Text
    label2, свойствоЗначение
    Name lblIsComplete
    Location 104; 56
    Size 168; 23
    Text
    label3, свойствоЗначение
    Name lblCallback
    Location 104; 96
    Size 168; 23
    Text

    Переходим в код формы. Создаем делегат AsynchronousMethodDelegate:

    private delegate string AsynchronousMethodDelegate(ActivateType source);

    Создаем метод, предназначенный для асинхронного запуска:

    private string AsynchronousMethod(ActivateType source)
    	{
    		string result = String.Empty;
    		switch(source)
    		{
    			case ActivateType.Callback:
    				result = "Callback";
    				break;
    			case ActivateType.End:
    				result = "End";
    				break;
    			case ActivateType.IsComplete:
    				result = "Is Complete";
    				break;
    		}
    		return result;
    	}

    В зависимости от выбора значения перечисления ActivateType метод будет возвращать одну из трех переменных типа stringCallback, End или Is Complete. Выбранное значение перечисления будет типом завершения вызова:

    private enum ActivateType
    	{
    		End,
    		IsComplete,
    		Callback
    	}

    В обработчике кнопки btnEnd выводим результат работы асинхронного метода на надпись lblEnd:

    private void btnEnd_Click(object sender, System.EventArgs e)
    	{
    		AsynchronousMethodDelegate asyncDeleg = new 
    			AsynchronousMethodDelegate(AsynchronousMethod);
    		// Запускаем асинхронно метод AsynchronousMethod.
    		IAsyncResult ar = asyncDeleg.BeginInvoke(ActivateType.End, null, null);
    		// Получаем  результаты асинхронного метода.
    		lblEnd.Text = asyncDeleg.EndInvoke(ar);
    	}

    В обработчике кнопки btnIsComplete применяем способ завершения работы Pooling и также выводим результат на надпись lblIsComplete:

    private void btnIsComplete_Click(object sender, System.EventArgs e)
    	{
    		AsynchronousMethodDelegate asyncDeleg = new 
    			AsynchronousMethodDelegate(AsynchronousMethod);
    	// Запускаем асинхронно метод AsynchronousMethod.
    		IAsyncResult ar = asyncDeleg.BeginInvoke(ActivateType.IsComplete, null, null);
    	while(!ar.IsCompleted)
    		{
    		lblIsComplete.Text = "Ожидание";
    		}
    		// Получаем  результаты асинхронного метода.
    		lblIsComplete.Text = asyncDeleg.EndInvoke(ar);
    	}

    В обработчике кнопки применяем способ завершения работы Callback:

    private void btnCallback_Click(object sender, System.EventArgs e)
    	{
    		AsynchronousMethodDelegate asyncDeleg = new 
    			AsynchronousMethodDelegate(AsynchronousMethod);
    		// Создаем экземпляр callback  делегата Callback.
    		AsyncCallback callback = new AsyncCallback(CallbackMethod);
    			// Запускаем асинхронно метод AsynchronousMethod.
    		asyncDeleg.BeginInvoke(ActivateType.Callback, callback, asyncDeleg);
    	}

    Добавляем метод для завершения вызова и выводим результат на надпись lblCallback:

    private void CallbackMethod(IAsyncResult ar)
    	{
    		// Приводим  к типу AsynchronousMethodDelegate
    AsynchronousMethodDelegate asyncDeleg = (AsynchronousMethodDelegate)ar.AsyncState;
    			// Получаем  результаты асинхронного метода.
    		lblCallback.Text = asyncDeleg.EndInvoke(ar);
    	}

    Запускаем приложение. Его работа предельно проста — при нажатии на кнопки запускается метод AsynchronousMethod, который завершается одним из асинхронных способов (рис. 7.4).

    (рис 7.4) Приложение AsyncMethods

    На диске, прилагаемом к книге, вы найдете приложение AsyncMethods (Code\Glava7\AsyncMethods).

    Обновление пользовательского интерфейса в Windows-приложениях

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

    Создайте новое Windows-приложение и назовите его TestMethodInvoker. Перетаскиваем на форму три кнопки (точное расположение их не важно) и свойству Name устанавливаем значения btnAddSync, btnAsync, btnAddAsyncMI (в соответствии со свойством Text на рис. 7.5).

    (рис 7.5) Приложение TestMethodInvoker в режиме дизайна

    В Windows-приложениях перед обновлением пользовательского интерфейса необходимо вернуть элемент управления в основной поток. Это делается с помощью делегата MethodInvoker, который не принимает и не возвращает параметров.

    Чтобы инициализировать MethodInvoker-делегат, нужно создать метод, обновляющий пользовательский интерфейс:

    // Метод, обновляющий пользовательский интерфейс
    private void UpdateUI()	
    {}
    // Инициализация делегата MethodInvoker
    MethodInvoker methodinvoker = new MethodInvoker(UpdateUI);

    Для запуска экземпляра methodinvoker делегата MethodInvoker асинхронно — вызываем метод BeginInvoke текущей формы:

    // Асинхронный запуск 
    this.BeginInvoke(methodinvoker);

    Добавляем обработчиков кнопок:

    private void btnAddSync_Click(object sender, System.EventArgs e)
    {
    // Синхронный запуск метода UpdateUI
    UpdateUI();		
    }
    	
    // Делегат для вызова метода UpdateUI асинхронно.
    delegate void AddItemAsyncDelegate();
    
    private void btnAddAsyncMI_Click(object sender, System.EventArgs e)
    {
    // Асинхронный вызов метода UpdateAsync
    AddItemAsyncDelegate asyncDel = new AddItemAsyncDelegate(UpdateAsync);
    asyncDel.BeginInvoke(null, null);
    }
    private void UpdateAsync()
    {
    // Асинхронный вызов метода UpdateUI
    MethodInvoker methodinvoker = new MethodInvoker(UpdateUI);
    this.BeginInvoke(methodinvoker);
    }
    private void UpdateUI()
    {
    // Добавление элемента управления.
    Button btn = new Button();
    btn.Text = "Кнопка";
    this.Controls.Add(btn);
    }
    		
    
    private void btnAsync_Click(object sender, System.EventArgs e)
    {
    // Асинхронный вызов метода UpdateUI
    // При выполнении этого метода элемент управления на форму 
    // не добавится, поскольку он создан не в основном потоке.
    AddItemAsyncDelegate asyncDel = new AddItemAsyncDelegate(UpdateUI);
    asyncDel.BeginInvoke(null, null);
    }

    Запускаем приложение. При нажатии кнопки Sync кнопка появляется самым обычным способом — в ее обработчике просто вызывается метод UpdateUI. В обработчиках кнопок Async и Async with MI вызывается метод UpdateUI, только в первом случае элемент создается не в основном потоке, поэтому не происходит обновления пользовательского интерфейса, а во втором случае элемент возвращается в основной поток – и кнопка появляется на форме.

    Использование делегата MethodInvoker необходимо только в Windows-приложениях.

    На диске, прилагаемом к книге, вы найдете приложение TestMethodInvoker (Code\Glava7\ TestMethodInvoker).

    Меры безопасности в многопоточных приложениях

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

    Самые распространенные проблемы, возникающие при использовании многопоточной модели программирования, — это "Гонка" (Race condition) и "Мертвая точка" (Deadlock).

    Проблема "Гонки" встречается, когда два или более потоков начинают использовать одновременно один объект. Например, в то время как один поток считывает данные из структуры, другой может начать изменять ее. После выполнения этих операций неизвестно, какие данные считал первый поток: старые, новые или перемешанные.

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

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

    Одним из решений этих проблем является закрытие общего доступа к объектам между потоками. Но иногда приходится использовать одни переменные в многопоточных приложениях. Для таких целей в пространстве имен System.Threading находятся классы, помогающие осуществить синхронизацию объектов.

    Автоматическая синхронизация

    Библиотека .NET Framework предоставляет автоматическую синхронизацию объектов. Для этого служит атрибут SynchronizationAttribute, находящийся в пространстве имен System.Runtime.Remoting.Contexts. Его можно установить на любой класс, унаследованный от класса ContextBoundObject. При этом все свойства и методы этого класса будут синхронизированы, т.е. только один поток сможет работать с объектом этого класса. Для использования объектов внутри этого класса несколькими потоками следует их сделать статическими, т. е. установить свойство static. Пример использования этого атрибута:

    using System;
    using System.Runtime.Remoting.Contexts;
    
    namespace AutomaticSynchronization
    {	
    	[Synchronization()]
    	class MyClass:ContextBoundObject
    	{
    		string myName;
    		/// <summary>
    		/// Конструктор
    		/// </summary>
    		/// <param name="name">Имя.</param>
    		public MyClass(string name)
    		{
    			this.myName = name;
    		}
    		/// <summary>
    		/// Параллельные потоки НЕ могут 
    		/// одновременно использовать свойство Name
    		/// </summary>
    		public string Name
    		{
    			get { return myName;}
    			set { myName = value;}
    		}
    		static int myAge;
    		/// <summary>
    		/// Статический конструктор.
    		/// </summary>
    		static MyClass()
    		{
    			myAge = 15;
    		}
    		/// <summary>
    		/// Возраст. Статическое свойство. Параллельные потоки могут 
    		/// одновременно использовать это свойство.
    		/// </summary>
    		public static int Age
    		{
    			get{ return myAge;}
    			set{ myAge = value;}
    		}
    	}
    }

    Использование областей синхронизации

    Следующим способом синхронизации является использование ключевого слова lock. Его использует класс Monitor из пространства имен System.Threading для блокировки области кода. Это ключевое слово может применяться и в статических методах. Оно имеет следующий синтаксис:

    lock(объект) блок выражений.

    Пример использования ключевого слова lock:

    public void Function()
    {
        System.Object lockThis = new System.Object();
        lock(lockThis)
        {
    // Код, который нуждается в эксклюзивных правах
        }
    }

    В процессе компиляции приложения среда заменяет выражение lock на блок try, finally:

    SomeClass x = new SomeClass();
    lock(x)
    {
        // Выполнение действий.
        DoSomething();
    }
    
       	// Эквивалентный код:
    
    SomeClass x = new SomeClass();
    System.Object obj = (System.Object)x;
    System.Threading.Monitor.Enter(obj);
    try
    {
        // Выполнение действий.
        DoSomething();
    }
    finally
    {
        System.Threading.Monitor.Exit(obj);
    }

    В статических методах lock можно включать прямо внутри свойств:

    using System;
    using System.Threading;
    
    class Cache
    {
    public static void Add(object x)
    {
    // Код, который не нуждается в эксклюзивных правах
    lock (typeof(Cache))
    {
    // Код, который нуждается в эксклюзивных правах
    }
    // Код, который не нуждается в эксклюзивных правах
    }
    }

    Ручная синхронизация

    Библиотека .NET Framework предлагает еще один способ синхронизации — применение атрибута MethodImplAttribute, принадлежащего пространству имен System.Runtime.CompilerServices. В отличие от ключевого слова lock или класса Monitor, действие этого атрибута распространяется не на отдельный кусок кода, а на весь метод. Его можно использовать как с методами объекта, так и со статическими методами. Например, класс SomeClass имеет два метода с синхронизацией доступа, один из который статический:

    using System;
    using System.Threading;
    using System.Runtime.CompilerServices;
    
    namespace Sample
    {
    	
    	/// <summary>
    	/// Применение  атрибута MethodImplAttribute
    	/// </summary>
    	public class SomeClass
    	{
    		/// <summary>
    		/// Статическая переменная.
    		/// </summary>
    		static int staticValue = 0;
    		/// <summary>
    		/// Переменная экземпляра.
    		/// </summary>
    		int instanceValue = 0;
    		/// <summary>
    		/// Синхронизированный доступ к переменной экземпляра.
    		/// </summary>
    		[MethodImplAttribute(MethodImplOptions.Synchronized)]
    		public void ChangeInstance()
    		{
    			// Действия метода.
    		}
    		/// <summary>
    		/// Синхронизированный доступ к статической переменной
    		/// </summary>
    		[MethodImplAttribute(MethodImplOptions.Synchronized)]
    		public static void ChangeStatic()
    		{
    			// Действия метода.
    		}
    	}
    }

    В приведенном примере с web-службой используется техника callback для завершения вызова метода HelloWorld. Обратите внимание, что web-служба автоматически создает асинхронные варианты всех методов, помеченных атрибутом [WebMethod].

    Подключение к web-сервису

    Web-сервисы представляют собой отдельные компоненты, к которым можно получить доступ через Интернет и задействовать их в своем web-приложении. Их использование напоминает подключение компонент .NET или СOM в Windows-приложениях и зачастую существенно облегчает разработку. Более подробную информацию о web-сервисах вы сможете найти в литературе, посвященной технологии ASP.NET.

    Подключение к web-сервисам — это, как правило, длительный процесс, занимающий несколько секунд. Рассмотрим два варианта подключения к сервису: один будет подключаться к web-службе синхронно, другой — асинхронно.

    Создадим вначале web-сервис. В Visual Studio .NET создаем новый проект, в окне шаблонов выбираем ASP.NET Web Service и называем его HelloWorldService (рис. 7.6).

    (рис 7.6) Создание web-сервиса

    Переключаемся в режим кода и убираем комментарии с существующего метода HelloWorld (рис. 7.7).

    (рис 7.7) Режим кода HelloWorldService

    Добавляем в тело метода следующий код:

    [WebMethod]
    	public string HelloWorld()
    	{
    		// Имитация длительной работы метода
    		System.Threading.Thread.Sleep(2000);
    		return "Hello World";
    }

    Компилируем проект и закрываем его. Теперь займемся Windows-приложением, которое будет вызывать Web-Service. Создайте новый проект и назовите его WebService. В окне Solution Explorer щелкаем правой кнопкой на папке References и в появившемся меню выбираем пункт Add Web Reference (рис. 7.8).

    (рис 7.8) Приложение WebService в режиме дизайна. Добавление ссылки на web-сервис

    В появившемся окне нажимаем на ссылку Web Services on the local machine (рис. 7.9).

    (рис 7.9) Окно Add Web Reference

    Из появившегося списка выбираем только что созданный web-сервис и переходим по ссылке (рис. 7.10).

    (рис 7.10)

    В поле Web Reference Name вводим название WebService и нажимаем Add Reference. Название web-cервиса будет названием proxy-класса (рис. 7.11).

    (рис 7.11) Свойства выбранного сервиса

    После добавления сервиса к приложению в окне Solution Explorer появляется ссылка на него.

    Из окна Toolbox перетаскиваем на форму две кнопки и надпись. Расположите их так, как показано на рис. 7.7. Называем кнопки btnSync и btnAsync соответственно, а надпись — lblHelloWorld. В обработчике кнопки btnSync будем вызывать сервис синхронно, т. е. обычным способом:

    private void btnSync_Click(object sender, System.EventArgs e)
    	{
    		// Деактивируем кнопку.
    		btnSync.Enabled = false;
    		// Создаем  экземпляр Web-службы
    		WebService.Service1 service = new 
    			WebService.Service1();
    		// Запуск метода
    		lblHelloWorld.Text = service.HelloWorld();
    		// Активируем  кнопку.
    		btnSync.Enabled = true;
    
    	}

    Для асинхронного запуска добавляем обработчик кнопки btnAsync и метод AsyncCallBackMethod:

    private void btnAsync_Click(object sender, System.EventArgs e)
    	{
    		// Деактивируем кнопку.
    		btnAsync.Enabled = false;
    		// Создаем  экземпляр web-службы
    		WebService.Service1 service = new WebService.Service1();			
    		// Запуск метод асинхронно
    		service.BeginHelloWorld(new AsyncCallback(AsyncCallBackMethod), service);
    
    	}
    	private void AsyncCallBackMethod(IAsyncResult ar)
    	{
    		WebService.Service1 service = (WebService.Service1)ar.AsyncState;
    		lblHelloWorld.Text = service.EndHelloWorld(ar);
    		// Активируем  кнопку.
    		btnAsync.Enabled = true;
    	}

    Запускаем проект и нажимаем на кнопку Sync. Кнопка становится неактивной на время подключения к web-сервису, затем выводится строка Hello World — содержимое метода HelloWorld. В течение подключения приложение зависает; попробуйте перемещать форму по экрану. Нажимаем на кнопку btnAsync — в этом случае форма отвечает на запросы, поскольку основной поток свободен, а выполнение метода web-службы происходит в параллельном потоке.

    На диске, прилагаемом к книге, вы найдете приложение WebService (Code\Glava7\ WebService).

    Элемент управления ProgressBar

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

    button1, свойствоЗначение
    Name btnEnter
    Location 128; 216
    Text Ввод
    button2, свойствоЗначение
    Name btnStart
    Location 208; 216
    Text Пуск
    textBox1, свойствоЗначение
    Location 16; 216
    Text
    progressBar1, свойствоЗначение
    Location 16; 32
    Maximum 30000
    Size 264; 23
    pictureBox1, свойствоЗначение
    Image Code\Glava7\ProgressBar\Image\FILECOPY.gif
    Maximum 30000
    Location 16; 88
    Size 264; 72
    Visible False

    Элемент ProgressBar имеет свойство Value, принимающее численное значение, которое отображается на шкале заполнения. Мы установили максимальное значение этой шкалы 30000. При нажатии на кнопку "Ввод" значение, введенное в текстовое поле, будет устанавливаться на шкале:

    private void btnEnter_Click(object sender, System.EventArgs e)
     {
    
       try
       {
    	   progressBar1.Value = Convert.ToInt32(textBox1.Text);
    				
    				
       }
    	 catch(Exception)
      {
    	MessageBox.Show("При выполнении преобразования типов возникла ошибка");
    	return;
    	}
     }

    При нажатии на кнопку "Пуск" будет запускаться простой процесс, который будет отражать ProgressBar, а также появится стандартный рисунок копирования:

    private void btnStart_Click(object sender, System.EventArgs e)
     {
       pictureBox1.Visible = true;
       for(int i =0; i<30000; i++)
       {
    	  progressBar1.Value = i;
       }
     }

    Запускаем приложение (рис. 7.12). Процесс выполняется в синхронном режиме — попробуйте перемещать форму в момент запуска.

    (рис 7.12) Приложение ProgressBar

    Даже такое простое приложение требуется запускать в асинхронном режиме. Переделайте его самостоятельно, используя в качестве примера приложение ArbitraryMethod.

    На диске, прилагаемом к книге, вы найдете приложение ProgressBar (Code\Glava7\ ProgressBar).

    Вернуться к учебному плану