Дополнительные возможности AsyncEnumerator - Отмена

ОГЛАВЛЕНИЕ

Отмена

AsyncEnumerator позволяет внешнему коду прерывать работу итератора. Эта функция особенно полезна для приложений Windows Forms и WPF, поскольку она позволяет нетерпеливому пользователю отменить идущую операцию и возвратить себе контроль над приложением. AsyncEnumerator также способен прекратить собственную работу после указанного промежутка времени. Эта функция полезна для серверных приложений, которые стремятся ограничить объем времени, которого требует ответ на запрос клиента. Методы, относящиеся к отмене, показаны ниже:

public class AsyncEnumerator {
  // Call this method from inside the iterator
  public Boolean IsCanceled(out Object cancelValue);

  // Call this method from inside the iterator
  public Boolean IsCanceled();

  // Call this method from code outside the iterator
  public Boolean Cancel(Object cancelValue};

  // Call this method from code inside or outside the iterator
  public void SetCancelTimeout(Int32 milliseconds,
   Object cancelValue);
}

Чтобы воспользоваться отменой, внутри своего итератора создавайте каждую асинхронную операцию как часть группы отказа. Это позволяет AsyncEnumerator автоматически отказываться от любых операций, которые завершаются после запроса на отмену. Затем, для обнаружения запроса на отмену, включите код, подобный показанному на рис. 3, после каждого оператора yield return.

Рис. 3. Обнаружение отмены

IEnumerator<Int32> MyIterator(AsyncEnumerator ae, ...) {
  // obj refers to some object that has BeginXxx/EndXxx methods
  obj.BeginXxx(...,     // Pass any arguments as usual
   ae.End(0, obj.EndXxx), // For AsyncCallback indicate
               // discard group 0 & proper End method  
               //to call for cleanup
   null);         // BeginXxx's AsyncState argument

  // Make more calls to BeginXxx methods if desired here...

  yield return n; // Resume iterator after 'n' operations
          // complete or if cancelation occurs 

  // Check for cancellation
  Object cancelValue;
  if (ae.IsCanceled(out cancelValue)) {
   // The iterator should cancel due to user request/timeout
   // Note: It is common to call "yield break" here.
  } else {
   // Call DequeueAsyncResult 'n' times to 
   // process the completed operations
  }
}

Теперь, когда требуется начать исполнение отменяемого итератора, создается объект AsyncEnumerator, и на нем вызывается объект BeginExecute, точно так же, как это делается обычно. Затем, когда какой-то части приложения требуется прервать работу итератора, она вызывает метод Cancel («Отмена»). При вызове Cancel можно передать ему ссылку на объект, которая затем передается итератору при помощи выходного параметра метода IsCanceled. Этот объект дает коду, отменяющему работу итератора, способ сообщить итератору, почему она отменяется. Если для итератора не важно, почему он отменяется, он может вызвать перегруженный метод IsCanceled, который не принимает параметров.

Метод SetCancelTimeout может быть вызван кодом как изнутри, так и снаружи итератора. Когда вызван этот метод, он устанавливает таймер, который автоматически вызовет Cancel (передавая значение, указываемое через аргумент CancelValue) по истечении времени.

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

Рис. 4. WindowsFormsViaAsyncEnumerator.cs

namespace WinFormUsingAsyncEnumerator {
  public partial class WindowsFormsViaAsyncEnumerator : Form {
   public static void Main() {
    Application.Run(new WindowsFormsViaAsyncEnumerator());
   }

   public WindowsFormsViaAsyncEnumerator() {
    InitializeComponent();
   }

   private AsyncEnumerator m_ae = null;

   private void m_btnStart_Click(object sender, EventArgs e) {
    String[] uris = new String[] {
     "http://Wintellect.com/", 
     "http://1.1.1.1/",  // Demonstrates error recovery
     "http://www.Devscovery.com/" 
    };

    m_ae = new AsyncEnumerator();

    // NOTE: The AsyncEnumerator automatically saves the 
    // Windows Forms SynchronizationContext with it ensuring
    // that the iterator always runs on the GUI thread; 
    // this allows the iterator to access the UI Controls

    // Start iterator asynchonously so that GUI thread doesn't block
    m_ae.BeginExecute(GetWebData(m_ae, uris), m_ae.EndExecute);
   }

   private IEnumerator<Int32> GetWebData(AsyncEnumerator ae, String[] uris) {
    ToggleStartAndCancelButtonState(false);
    m_lbResults.Items.Clear();

    if (m_chkAutoCancel.Checked)
     ae.SetCancelTimeout(5000, ae);

    // Issue several Web requests (all in discard group 0) simultaneously
    foreach (String uri in uris) {
     WebRequest webRequest = WebRequest.Create(uri);

     // If the AsyncEnumerator is canceled, DiscardWebRequest cleans up
     // any outstanding operations as they complete in the future
     webRequest.BeginGetResponse(ae.EndVoid(0, DiscardWebRequest), 
                              webRequest);
    }

    yield return uris.Length; // Process the completed Web requests 
                 // after all complete

    String resultStatus; // Ultimate result of processing shown to user

    // Check if iterator was canceled
    Object cancelValue;
    if (ae.IsCanceled(out cancelValue)) {
     ae.DiscardGroup(0);
     // Note: In this example calling DiscardGroup above is not mandatory
     // because the whole iterator is stopping execution; causing all
     // discard groups to be discarded automatically.

     resultStatus = (cancelValue == ae) ? "Timeout" : "User canceled";
     goto Complete;
    }

    // Iterator wasn't canceled, process all the completed operations
    for (Int32 n = 0; n < uris.Length; n++) {
     IAsyncResult result = ae.DequeueAsyncResult();

     WebRequest webRequest = (WebRequest)result.AsyncState;

     String s = "URI=" + webRequest.RequestUri + ", ";
     try {
      using (WebResponse webResponse = webRequest. 
         EndGetResponse(result)) {
           s += "ContentLength=" + webResponse.ContentLength;
      }
     }
     catch (WebException e) {
      s += "Error=" + e.Message;
     }
     m_lbResults.Items.Add(s); // Add result of operation to listbox
    }
    resultStatus = "All operations completed.";

   Complete:
    // All operations have completed or cancellation occurred, tell   // user
    MessageBox.Show(this, resultStatus);

    // Reset everything so that the user can start over if they desire
    m_ae = null;  // Reset since we're finished
    ToggleStartAndCancelButtonState(true);
   }

   private void m_btnCancel_Click(object sender, EventArgs e) {
    m_ae.Cancel(null);
    m_ae = null;
   }

   // Swap the Start/Cancel button states
   private void ToggleStartAndCancelButtonState(Boolean enableStart) {
    m_btnStart.Enabled = enableStart;
    m_btnCancel.Enabled = !enableStart;
   }

   private void DiscardWebRequest(IAsyncResult result) {
    // Get the WebRequest object used to initate the request 
    // (see BeginGetResponse's last argument)
    WebRequest webRequest = (WebRequest)result.AsyncState;

    // Clean up the async operation and Close the WebResponse (if no    // exception)
    webRequest.EndGetResponse(result).Close();
   }
  }
}

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

Этот образец выполняет веб-запросы, используя класс WebRequest из .NET. При вызове метода BeginGetResponse класса WebRequest очистка требует не просто вызова EndGetResponse. Необходимо также вызвать метод Close («Закрыть») или Dispose («Удалить») на объекте WebResponse, возвращаемом EndGetResponse.

По этой причине код передает метод DiscardWebRequest методу EndVoid при вызове BeginGetResponse. Метод DiscardWebRequest гарантирует, что объект WebResponse закрыт, если выполнение веб-запроса было успешным и не привело к исключению.

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

Использование функции итератора C# и моего класса AsyncEnumerator позволяет разработчикам применить асинхронное программирование изнутри синхронной модели программирования. AsyncEnumerator также легко интегрируется с другими частями .NET Framework и предлагает множество функций, позволяющих разработчикам в своих приложениях выходить за пределы того, что возможно с помощью обычной синхронной модели программирования.
Я использую AsyncEnumerator уже более года и помог многим компаниям интегрировать его в их программное обеспечение с превосходными результатами. Загрузите код с wintellect.com/PowerThreading.aspx. Я надеюсь, что вы извлечете из него не меньше пользы, чем я.

Скачать исходники примеров кода 

Автор: Джеффри Рихтер (Jeffrey Richter)