Рекомендации по реализации асинхронной модели, основанной на событиях

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

Общие сведения см. в разделе Рекомендации по реализации асинхронной модели, основанной на событиях.

Обеспечение требуемого поведения

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

Completion

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

Событие Completed и EventArgs

Для каждого отдельного метода имя_методаAsync придерживайтесь следующих требований:

  • Определите событие имя_методаCompleted в том же классе, что и этот метод.

  • Определите класс EventArgs и сопроводительный делегат для события имя_методаCompleted, наследованного от класса AsyncCompletedEventArgs. Имя класса по умолчанию должно быть представлено в таком формате: имя_методаCompletedEventArgs.

  • Убедитесь, что класс EventArgs связан с возвращаемыми значениями метода имя_метода. При использовании класса EventArgs вы никогда не должны требовать от разработчиков выполнить приведение результата.

    В следующем примере кода показана хорошая и плохая реализация этого требования к разработке.

// Good design
private void Form1_MethodNameCompleted(object sender, xxxCompletedEventArgs e)
{
    DemoType result = e.Result;
}

// Bad design
private void Form1_MethodNameCompleted(object sender, MethodNameCompletedEventArgs e)
{
    DemoType result = (DemoType)(e.Result);
}
  • Не определяйте класс EventArgs для возвратных методов, которые возвращают void. Вместо этого следует использовать экземпляр класса AsyncCompletedEventArgs.

  • Следите за тем, чтобы всегда создавалось событие имя_методаCompleted. Это событие должно возникать при успешном завершении, ошибке или отмене. Приложения никогда не должны попадать в ситуацию, в которой они остаются неактивными, а завершение не выполняется.

  • Обязательно перехватывайте любые исключения, возникающие в асинхронной операции, и назначайте их свойству Error.

  • Если при выполнении задачи возникла ошибка, результаты должны быть недоступны. Когда свойство Error имеет значение, отличное от null, убедитесь, что обращение к любому свойству в структуре EventArgs вызывает исключение. Для выполнения этой проверки используйте метод RaiseExceptionIfNecessary.

  • Смоделируйте истечение времени ожидания в качестве ошибки. Когда время ожидания истечет, вызовите событие имя_методаCompleted и назначьте свойству Error значение TimeoutException.

  • Если класс поддерживает несколько одновременных вызовов, убедитесь, что событие имя_методаCompleted содержит соответствующий объект userSuppliedState.

  • Убедитесь, что событие имя_методаCompleted создается в правильном потоке и в правильном периоде жизненного цикла приложения. Дополнительные сведения см. в разделе "Потоки и контексты".

Одновременное выполнение операций

  • Если класс поддерживает несколько одновременных вызовов, предоставьте разработчику возможность отслеживать каждый вызов по отдельности, определив перегрузку имя_методаAsync, которая принимает объект состояния или идентификатор задачи в параметре с именем userSuppliedState. Этот параметр должен всегда стоять последним в сигнатуре метода имя_методаAsync.

  • Если класс определяет перегрузку имя_методаAsync, которая принимает параметр с объектом состояния или идентификатором задачи, обязательно отслеживайте время существования операции с этим идентификатором задачи и возвращайте его в завершающий обработчик. Можно использовать доступные вспомогательные классы. Дополнительные сведения об управлении параллелизмом см. в практическом руководстве по реализации компонента, поддерживающего асинхронную модель на основе событий.

  • Если класс определяет метод имя_методаAsync без параметра состояния и не поддерживает несколько одновременных вызовов, обязательно создавайте исключение InvalidOperationException при любой попытке вызова имя_методаAsync до завершения предыдущего вызова имя_методаAsync.

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

Доступ к результатам

  • Если во время выполнения асинхронной операции возникла ошибка, результаты должны быть недоступны. Убедитесь, что при обращении к любому свойству в AsyncCompletedEventArgs, когда Error имеет значение, отличное от null, возникает исключение, на которое ссылается Error. Для этой цели класс AsyncCompletedEventArgs предоставляет метод RaiseExceptionIfNecessary.

  • Убедитесь, что любая попытка доступа к результату вызывает исключение InvalidOperationException, указывающее на отмену операции. Для выполнения этой проверки используйте метод AsyncCompletedEventArgs.RaiseExceptionIfNecessary.

Отчет о состоянии

  • По возможности реализуйте поддержку отчетов о ходе выполнения. Это позволяет разработчикам улучшить взаимодействие приложения с пользователем при использовании вашего класса.

  • Если вы реализуете событие ProgressChanged или имя_методаProgressChanged, убедитесь, что такие события не создаются для конкретной асинхронной операции после того, как было создано событие имя_методаCompleted.

  • Если выполняется заполнение стандартного ProgressChangedEventArgs, убедитесь, что ProgressPercentage всегда можно интерпретировать как процент. Этот процент может быть неточным, однако он всегда должен представлять именно процент. Если ваша метрика отчетов о ходе выполнения должна отличаться от процента, создайте производный класс от класса ProgressChangedEventArgs и оставьте для ProgressPercentage значение 0. Старайтесь не использовать метрику отчетов, отличную от процента.

  • Убедитесь, что событие ProgressChanged возникает в соответствующем потоке и в соответствующее время жизненного цикла приложения. Дополнительные сведения см. в разделе "Потоки и контексты".

Реализация IsBusy

  • Не предоставляйте свойство IsBusy, если ваш класс поддерживает несколько одновременных вызовов. Например, прокси-серверы XML-веб-службы не предоставляют свойство IsBusy, так как поддерживают несколько одновременных вызовов асинхронных методов.

  • Свойство IsBusy должно возвращать true после того, как был вызван метод имя_методаAsync, но еще не было создано событие имя_методаCompleted. В противном случае оно должно возвратить false. Компоненты BackgroundWorker и WebClient являются примерами классов, которые предоставляют свойство IsBusy.

Отмена

  • По возможности реализуйте поддержку отмены. Это позволяет разработчикам улучшить взаимодействие приложения с пользователем при использовании вашего класса.

  • В случае отмены установите флаг Cancelled в объекте AsyncCompletedEventArgs.

  • Убедитесь, что любая попытка доступа к результату вызывает исключение InvalidOperationException, указывающее на отмену операции. Для выполнения этой проверки используйте метод AsyncCompletedEventArgs.RaiseExceptionIfNecessary.

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

  • При отмене операции создайте событие имя_методаCompleted.

Ошибки и исключения

  • Перехватывайте любые исключения, возникающие в асинхронной операции, и назначайте их в качестве значения свойства AsyncCompletedEventArgs.Error.

Потоки и контексты

Для правильной работы вашего класса крайне важно, чтобы обработчики событий клиента вызывались в правильном потоке или контексте для заданной модели приложения, включая приложения Windows Forms и ASP.NET. Доступно два важных вспомогательных класса, позволяющих проверить, правильно ли себя ведет ваш асинхронный класс при любой модели приложения: AsyncOperation и AsyncOperationManager.

AsyncOperationManager предоставляет один метод CreateOperation, который возвращает AsyncOperation. Метод имя_методаAsync вызывает CreateOperation, а класс использует возвращенное значение AsyncOperation, чтобы отследить время существования асинхронной задачи.

Чтобы сообщить клиенту о ходе выполнения, добавочных результатах и завершении, выполните вызов методов Post и OperationCompleted для AsyncOperation. AsyncOperation отвечает за маршалирование вызовов обработчиков событий клиента в соответствующий поток или контекст.

Примечание.

Вы можете не соблюдать эти правила, если хотите явно нарушить политику модели приложения, но при этом воспользоваться другими преимуществами асинхронной модели на основе событий. Например, вам может понадобиться, чтобы класс, работающий в Windows Forms, был со свободным потоком. Вы можете создать класс со свободным потоком, если разработчики осознают накладываемые этим ограничения. Консольные приложения не синхронизируют выполнение вызовов Post. Это может вызвать беспорядочное возникновение событий ProgressChanged. Если вы хотите получить сериализованное выполнение вызовов Post, реализуйте и установите класс System.Threading.SynchronizationContext.

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

Рекомендации

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

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

  • Если вы реализуете метод с перегрузкой с несколькими вызовами (userState в сигнатуре), вашему классу потребуется управлять коллекцией пользовательских состояний (или идентификаторов задачи) и соответствующих им ожидающих операций. Эту коллекцию следует защитить областями lock, так как различные вызовы добавляют и удаляют объекты userState в ней.

  • Рекомендуется повторно использовать классы CompletedEventArgs, когда это возможно и уместно. В этом случае именование не соответствует имени метода, так как заданный делегат и тип EventArgs не привязаны к одному методу. Однако ни в коем случае нельзя принуждать разработчиков выполнить приведение значения, полученного из свойства в EventArgs.

  • Если вы создаете класс, являющийся производным от Component, не реализуйте и не устанавливайте свой собственный класс SynchronizationContext. Используемый SynchronizationContext определяют модели приложения, а не компоненты.

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

См. также