Обзор асинхронной модели, основанной на событиях

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

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

  • выполнять задачи, занимающие много времени, такие как загрузки и операции с базами данных, "в фоновом режиме" без прерывания работы приложения;

  • выполнять несколько операций одновременно, получая уведомления о завершении каждой из них;

  • ожидать освобождения ресурсов без остановки ("блокирования") приложения;

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

Класс, который поддерживает асинхронную модель на основе событий, будет иметь один или несколько методов с именем MethodNameAsync. Эти методы могут копировать синхронные версии, выполняющие ту же операцию в текущем потоке. Этот класс может содержать событие MethodNameCompleted, а также метод MethodNameAsyncCancel (или просто CancelAsync).

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

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

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

Внимание

Загрузка может закончиться одновременно с получением запроса CancelAsync, поэтому Cancelled может не отражать отмену запроса. Это называется состоянием гонки и является распространенной проблемой в многопоточном программировании. Дополнительные сведения о проблемах многопоточного программирования см. в разделе Рекомендации по работе с потоками.

Характеристики асинхронной модели на основе событий

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

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

Примеры для асинхронной модели на основе событий

Компоненты SoundPlayer и PictureBox представляют простые реализации асинхронной модели на основе событий. Компоненты WebClient и BackgroundWorker представляют более сложные реализации асинхронной модели на основе событий.

Ниже приведен пример объявления класса в соответствии с данной моделью:

Public Class AsyncExample  
    ' Synchronous methods.  
    Public Function Method1(ByVal param As String) As Integer
    Public Sub Method2(ByVal param As Double)
  
    ' Asynchronous methods.  
    Overloads Public Sub Method1Async(ByVal param As String)
    Overloads Public Sub Method1Async(ByVal param As String, ByVal userState As Object)
    Public Event Method1Completed As Method1CompletedEventHandler  
  
    Overloads Public Sub Method2Async(ByVal param As Double)
    Overloads Public Sub Method2Async(ByVal param As Double, ByVal userState As Object)
    Public Event Method2Completed As Method2CompletedEventHandler  
  
    Public Sub CancelAsync(ByVal userState As Object)
  
    Public ReadOnly Property IsBusy () As Boolean  
  
    ' Class implementation not shown.  
End Class  
public class AsyncExample  
{  
    // Synchronous methods.  
    public int Method1(string param);  
    public void Method2(double param);  
  
    // Asynchronous methods.  
    public void Method1Async(string param);  
    public void Method1Async(string param, object userState);  
    public event Method1CompletedEventHandler Method1Completed;  
  
    public void Method2Async(double param);  
    public void Method2Async(double param, object userState);  
    public event Method2CompletedEventHandler Method2Completed;  
  
    public void CancelAsync(object userState);  
  
    public bool IsBusy { get; }  
  
    // Class implementation not shown.  
}  

Вымышленный класс AsyncExample имеет два метода, которые поддерживают как синхронные, так и асинхронные вызовы. Синхронные перегрузки работают аналогично вызову любого метода и выполняют операцию в вызывающем потоке; если выполнение операции требует много времени, перед возвратом вызова может возникнуть значительная задержка. Асинхронные перегрузки запустят операцию в другом потоке и затем немедленно выполнят возврат, позволяя вызывающему потоку продолжить работу, пока операция выполняется "в фоновом режиме".

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

Для асинхронных операций существует две перегрузки: с одним вызовом и с несколькими вызовами. Различить две этих формы можно по их сигнатурам методов: форма с несколькими вызовами имеет дополнительный параметр с именем userState. Эта форма позволяет коду вызвать Method1Async(string param, object userState) несколько раз, не ожидая завершения ожидающих асинхронных операций. С другой стороны, если попытаться вызвать Method1Async(string param) до завершения предыдущего вызова, метод выдает исключение InvalidOperationException.

Параметр userState для перегрузок с несколькими вызовами позволяет вам различать асинхронные операции. Вы указываете уникальное значение (например, GUID или хэш-код) для каждого вызова Method1Async(string param, object userState), что позволяет обработчику событий после завершения каждой из операций определить, какой экземпляр этой операции сообщил о своем завершении.

Отслеживание ожидающих операций

Если вы используете перегрузки с несколькими вызовами, вашему коду потребуется отслеживать объекты userState (идентификаторы) для ожидающих задач. Для каждого вызова Method1Async(string param, object userState) обычно создается новый уникальный объект userState, который добавляется в коллекцию. Когда задача, соответствующая этому объекту userState, выдает событие выполнения, реализация метода выполнения проверяет AsyncCompletedEventArgs.UserState и удаляет его из вашей коллекции. При таком использовании параметр userState играет роль идентификатора задачи.

Примечание.

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

Отмена ожидающих операций

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

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

Методы, которые одновременно поддерживают только одну ожидающую операцию, такие как Method1Async(string param), не допускают отмену.

Получение обновлений ходы выполнения и добавочных результатов

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

Обработчик событий для события ProgressChanged может проверить свойство ProgressChangedEventArgs.ProgressPercentage, чтобы определить, какой процент асинхронной задачи уже выполнен. Это свойство находится в диапазоне от 0 до 100 и может использоваться для обновления свойства Value элемента ProgressBar. Если имеется несколько ожидающих асинхронных операций, вы можете использовать свойство ProgressChangedEventArgs.UserState, чтобы определить, какая операция сообщает о ходе выполнения.

Некоторые классы могут предоставлять добавочные результаты по мере выполнения асинхронных операций. Эти результаты будут сохранены в классе, который является производным от ProgressChangedEventArgs, и будут иметь вид свойств в производном классе. Вы можете получить доступ к этим результатам в обработчике событий для события ProgressChanged точно так же, как стали бы получать доступ к свойству ProgressPercentage. Если имеется несколько ожидающих асинхронных операций, вы можете использовать свойство UserState, чтобы определить, какая операция выдает добавочные результаты.

См. также