Асинхронный доступ к веб-службам в шаблонах проектирования .NET

ОГЛАВЛЕНИЕ

Описание реализации шаблона начало/конец (.NET 1) и событийно-управляемой модели (.NET 2).

•    Скачать демо-проект - 27.4 Кб

Введение

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

Для подключения к веб-службе можно сгенерировать класс-посредник с помощью инструмента Язык описания Веб-служб (WSDL) в .NET Framework или путем добавления веб-ссылки в Visual Studio. Посредник инкапсулирует все открытые методы, предоставляемые веб-службой, в виде синхронных и асинхронных функций. Подробней читайте в документации MSDN. Одна из имеющихся полезных статей – «Асинхронные вызовы веб-служб по HTTP с помощью .NET Framework» от Мэтта Повелла.

Асинхронная реализация преимущественно зависит от сгенерированного класса-посредника. .NET Framework предоставляет две асинхронных конструкции в посреднике. Одна – шаблон проектирования начало/конец из .NET 1.0, а вторая – событийно-управляемая модель, имеющаяся в .NET 2.0. В данной статье показаны обе реализации и описано несколько интересных и недокументированных проблем. Пример кода содержит простую веб-службу, клиент, собранный в VS 2003 для .NET 1.1, и другой клиент, собранный в VS 2005 для .NET 2.0.

Тестовая веб-служба

Так как веб-служба общая для платформы, веб-службу можно использовать независимо от ее происхождения или версии. Была создана тестовая служба в .NET 2.0, используемая обоими клиентами. Service.cs в следующем листинге-1 представляет службу с единственным методом GetStock(), принимающим символ и возвращающим его котировку.

// Листинг-1. Тестовая веб-служба в Service.cs

using System;
using System.Web.Services;
using System.Threading;

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]

public class Service : System.Web.Services.WebService
{
    Random  _delayRandom  = new Random();
    Random  _stockRandom = new Random();

    [WebMethod]
    public double GetStock(string symbol, int timeout)
    {
        int delay = _delayRandom.Next(50, 5000);
        Thread.Sleep(delay > timeout ? timeout : delay);

        if (delay > timeout) return -1;

        double value;
        switch (symbol)
        {
            case "MSFT": value = 27;  break;
            case "ELNK": value = 11;  break;
            case "GOOG": value = 350; break;
            case "SUNW": value = 6;   break;
            case "IBM":  value = 81;  break;
            default: value = 0; break;
        }

        return value + value * 0.1 * _stockRandom.NextDouble();
    }
}

Используются два объекта Random для моделирования действия службы. _delayRandom имитирует трафик по сети с 50 до 5000 миллисекунд, а _stockRandom предназначен для колебаний значения котировки. Служба позволяет клиенту установить timeout, второй параметр GetStock(). Поэтому, помимо возврата нормальной котировки, GetStock() также возвращает ноль для нераспознанного символа и минус один в качестве флага простоя.

Для простоты служба размещена на тестовом сервере VS 2005, как показано ниже:


 
Чтобы сделать это самостоятельно, вызовите WebDev.WebServer.exe с опцией для физического пути, где Service.asmx располагается так (смотрите startWsTest.bat в демо):

C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\WebDev.WebServer.EXE
/port:1111
/path:"c:\Articles\CallWsAsync\code\WebService2"
/vpath:"/callwsasync"

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

 

Как видно, первый - статус OK с возвращенной котировкой. Второй – произошло исключение, если сервер/сеть недоступна. Третий – для неверного символа. Четвертый возникает при щелчке по кнопке Отменить сразу, чтобы прервать запрос. Последний – ответ сервера на время простоя, определенное в пользовательском интерфейсе. В каждом сценарии время сессии регистрируется на экране. Данный снимок экрана показывает пользовательский интерфейс первого клиента, и оба клиента реализованы одинаково.

Шаблон проектирования начало/конец

Чтобы создать посредник в .NET 1.1, воспользуйтесь командой Добавить веб-ссылку в VS 2003, укажите URL для виртуального пути, например: http://localhost:1111/callwsasync/, и выберите Service.asmx. Посредник содержит BeginGetStock() и EndGetStock(). Назовите его WsClient1.WsTestRef1.Service и определите объект типа _wsProxy1. Листинг-2 ниже показывает, как работает первый клиент:

// Листинг-2. Использование шаблона начало/конец с обратным вызовом

private void buttonGetQuote_Click(object sender, System.EventArgs e)
{
    textBoxResult.Text = "";
    _tmStart = DateTime.Now.Ticks;

    string symbol = comboBoxSymb.Text.ToUpper();
    int timeout = int.Parse(textBoxTimeout.Text);

    _wsProxy1 = new WsClient1.WsTestRef1.Service();
    _wsProxy1.BeginGetStock(symbol, timeout,
            new AsyncCallback(OnGetStock), symbol);   
}

private void OnGetStock(IAsyncResult ar)
{
    string symbol = (string)ar.AsyncState;
    string result;
   
    try
    {
        double value = _wsProxy1.EndGetStock(ar);
        if (value ==0)
            result = "Invalid, " + "'" +symbol +"'";
        else
        if (value <0)
            result = "TimeOut, " + "[" +symbol +"]";
        else
            result = "OK, " + value.ToString("F");
    }
    catch (WebException e)
    {
        if (e.Status == WebExceptionStatus.RequestCanceled)
            result = "Cancelled, " + "<" +symbol +">";
        else
            result = "Exception, " + e.Message;
    }
    catch (Exception e)
    {
        result = "Exception, " + e.Message;
    }

     textBoxResult.Invoke(new ShowResultDelegate(ShowResult),
                                      new object[] {result});
    _wsProxy1 = null;
}
   
private void buttonCancel_Click(object sender, System.EventArgs e)
{
    if (_wsProxy1 != null)
        _wsProxy1.Abort();
}

private delegate void ShowResultDelegate(string str);
private void ShowResult(string str)
{
    textBoxResult.Text = str + ",  (" +
      (DateTime.Now.Ticks - _tmStart) / 10000 + " ms)";
}

В buttonGetQuote_Click() получаются символ и время простоя из диалогового окна и передаются в _wsProxy1.BeginGetStock(), чтобы запустить асинхронный запрос. Третий параметр BeginGetStock() инициализирует обратный вызов OnGetStock(). Передается последний параметр AsyncState и символ, чтобы вернуть позже в обратном вызове как индикатор. Как только BeginGetStock() вызван, можно отменить запрос, вызвав _wsProxy1.Abort() в buttonCancel_Click().

Посмотрите на OnGetStock(). В блоке try сначала вызывается EndGetStock() для возврата результата. Помните, ноль означает нераспознанный символ, отрицательное значение для времени простоя, а остальные считаются нормальной котировкой.

Заметьте, что отмена ловится в WebException. Если вызывается Abort() посредника, запрос прерывается с веб-исключением. Обратный вызов все же вызывается, и WebException генерируется из EndGetStock(). Оно обнаруживается путем проверки состояния RequestCanceled, чтобы отличить другие веб-исключения, такие как сервер/сеть не работает.

Асинхронный обратный вызов выполняется в другом потоке, неявно управляемом пулом потоков .NET. Обратный вызов может не быть в контексте потока, вызывающего BeginGetStock(). Будьте сознательны, когда пытаетесь отправить команды управляющему элементу формы или обратиться к объекту-экземпляру, определенному в классе. Для этого вызывается textBoxResult.Invoke() вместо установки textBoxResult.Text напрямую.\


Событийно-управляемая модель посредника

Она может показаться сложной при получении посредника в .NET 2.0 путем выполнения того же самого в VS 2005. Назовите этот посредник WsClient2.WsTestRef2.Service и определите объект как _wsProxy2. Подобно BeginGetStock() в .NET 1.1, этот посредник предоставляет GetStockAsync() как пусковой метод. Но надо добавить обработчик события, действующий как предыдущий обратный вызов. Следующий Листинг-3 показывает код второго клиента:

// Листинг-3. Использование событийно-управляемой модели

private void buttonGetQuote_Click(object sender, EventArgs e)
{
    textBoxResult.Text = "";
    _tmStart = DateTime.Now.Ticks;

    string symbol = comboBoxSymb.Text.ToUpper();
    int timeout = int.Parse(textBoxTimeout.Text);

    _wsProxy2 = new WsClient2.WsTestRef2.Service();
    _wsProxy2.GetStockCompleted += new
       GetStockCompletedEventHandler(OnGetStockCompleted);
    _wsProxy2.GetStockAsync(symbol, timeout, symbol);
}

private void OnGetStockCompleted(Object sender,
             GetStockCompletedEventArgs gca)
{
    string symbol = (string)gca.UserState;
    string result;

    if (gca.Cancelled)  // Вызывается CancelAsync
        result = "Cancelled2, " + gca.UserState;
    else
    if (gca.Error != null)
    {
        WebException webEx = gca.Error as WebException;
        if (webEx !=null && webEx.Status ==
                        WebExceptionStatus.RequestCanceled)
            result = "Cancelled, " + "<" + symbol + ">";
        else
            result = "Exception, " + gca.Error.Message;
    }
    else
    if (gca.Result == 0)
        result = "Invalid, " + "'" + symbol + "'";
    else
    if (gca.Result < 0)
        result = "TimeOut, " + "[" + symbol + "]";
    else
        result = "OK, " + gca.Result.ToString("F");

    textBoxResult.Text = result + ",  (" +
        (DateTime.Now.Ticks - _tmStart) / 10000 + " ms)";
    _wsProxy2 = null;
}

private void buttonCancel_Click(object sender, EventArgs e)
{
    if (_wsProxy2 != null)
    {
        //_wsProxy2.CancelAsync("<<" +
        //        comboBoxSymb.Text.ToUpper() + ">>");
        _wsProxy2.Abort();
    }
}

В buttonGetQuote_Click() добавляется обработчик события OnGetStockCompleted() в _wsProxy2 и вызывается GetStockAsync() для запуска асинхронного запроса. Аналогично, первые два параметра – символ и время простоя, а третий UserState, подобный AsyncState в BeginGetStock(), возвращается позже в обработчике. Запрос можно отменить с помощью последующего _wsProxy2.Abort().

Что нового в OnGetStockCompleted()? При вызове его второй параметр gca типа GetStockCompletedEventArgs (кратко смотрите его в листинге-4) приносит всю готовую информацию. gca содержит 4 свойства: UserState типа object, Result типа double, Error типа Exception и флаг Cancelled. Сравните с логикой в обратном вызове (Листинг-2), большинство частей понятны и нет нужды повторять.

Единственная хитрость касается gca.Cancelled. Как видно в Листинге-3, этот флаг сознательно проверяется в начале OnGetStockCompleted() и помещается другая проверка в состояние RequestCanceled из gca.Error. Что именно было захвачено? Определенно, успех - gca.Error, а не gca.Cancelled, потому что вызов _wsProxy2.Abort() приводит к генерации WebException.

Что если надо перехватить gca.Cancelled как флаг отмены? Немного изменим посредник. Чтобы отличить отмененный запрос от gca.Error, отображается «Отменено2» для gca.Cancelled следующим образом:

 

Используется CancelAsync() посредника, изначально ничего не делающий, но вызывающий его базовый. Поэтому в buttonCancel_Click() (в Листинге-3) вызывается _wsProxy2.CancelAsync(), а не _wsProxy2.Abort().

Листинг-4 ниже показывает измененный посредник, куда добавлено 4 нумерованных комментария, чтобы указать изменения:

// Листинг-4. Измененный событийно-управляемый посредник

public partial class Service : SoapHttpClientProtocol
{
    private System.Threading.SendOrPostCallback
                         GetStockOperationCompleted;
    private bool useDefaultCredentialsSetExplicitly;

    // 1. Добавлен управляющий флаг _done
    private bool _done = true;

    public Service() { ... }
    public new string Url { ... }
    public new bool UseDefaultCredentials { ... }
   
    public event GetStockCompletedEventHandler GetStockCompleted;

    [System.Web.Services.Protocols.SoapDocumentMethodAttribute(...)]
    public double GetStock(string symbol, int timeout)
    {
        object[] results = Invoke("GetStock",
             new object[] {symbol, timeout});
        return ((double)(results[0]));
    }

    ... ... ...
   
    public void GetStockAsync(string symbol, int timeout, object userState)
    {
        // 2. Инициализируется _dene – не готово
        _done = false;

        if (GetStockOperationCompleted == null)
        {
            GetStockOperationCompleted = new
              System.Threading.SendOrPostCallback(
              OnGetStockOperationCompleted);
        }
       
        this.InvokeAsync("GetStock", new object[] {symbol, timeout},
            GetStockOperationCompleted, userState);
    }
   
    private void OnGetStockOperationCompleted(object arg)
    {
        // 3. Когда завершено без отмены, возбуждается событие
        if (GetStockCompleted != null && !_done)
        {
            _done = true;
            InvokeCompletedEventArgs invokeArgs = (InvokeCompletedEventArgs)(arg);
            GetStockCompleted(this,
                   new GetStockCompletedEventArgs(invokeArgs.Results,
                                                  invokeArgs.Error,
                                                  invokeArgs.Cancelled,
                                                  invokeArgs.UserState));
        }
    }
   
    public new void CancelAsync(object userState)
    {
        // 4. Если готово, обработка не идет
        if (_done) return;

        // Вызвана отмена. Готово и возбуждается событие
        _done = true;
        GetStockCompleted(this,
                   new GetStockCompletedEventArgs(null,
                                                  null,
                                                  true,
                                                  userState));
        // base.CancelAsync(userState);
    }
   
    private bool IsLocalFileSystemWebService(string url) { ... }
}

[System.CodeDom.Compiler.GeneratedCodeAttribute( ... )]
public delegate void GetStockCompletedEventHandler(object sender,
                     GetStockCompletedEventArgs e);
... ...
public partial class GetStockCompletedEventArgs
     : System.ComponentModel.AsyncCompletedEventArgs
{
    private object[] results;
    internal GetStockCompletedEventArgs(object[] results,
                                        System.Exception exception,
                                        bool cancelled,
                                        object userState) :
            base(exception, cancelled, userState) { ... }
   
    public double Result { get { ... } }
}

Для ясности опущено большинство неважных участков. Во-первых, определяется флаг _done в классе и устанавливается в true, так как нет обрабатывающихся запросов. Во-вторых, в GetStockAsync(),_done инициализируется в false (не готово). GetStockAsync() регистрирует свой собственный внутренний обратный вызов OnGetStockOperationCompleted() и запускает асинхронный запрос с помощью InvokeAsync(). Как только OnGetStockOperationCompleted() вызывается для завершенного запроса, он возбуждает событие GetStockCompleted(), вызывающее обработчик OnGetStockCompleted(), добавленный ранее в _wsProxy2.

Затем, третье изменение происходит в OnGetStockOperationCompleted(). Добавляется проверка _done, чтобы запретить возбуждение, если _done уже установлен в true в CancelAsync(), что является следующим четвертым изменением. Как только пользователь вызывает CancelAsync() для отмены, если запрос завис (_done равен false), _done устанавливается в true и возбуждается событие отмены – событие отправляет аргумент GetStockCompletedEventArgs с флагом Cancelled, установленным в true.

Приведенный пример помогает понять, как работает событийно-управляемый посредник. Но в реальном производственном сценарии такое изменение посредника не рекомендуется. Если посредник восстанавливается позже, любые изменения кода будут потеряны. Поэтому желательно использовать Abort() вместо CancelAsync().


Централизованное управление службой

С помощью вышеприведенных реализаций можно запускать несколько асинхронных вызовов разных символов акций и получать результаты в одном обратном вызове или в одном обработчике события. Можно получить котировку для символа, обозначаемого AsyncState или членом UserState. Можно сделать это основанной на службе динамически подключаемой библиотекой (DLL) для нескольких вызывающих функций. Это прекрасно работает, пока каждый вызов создает свой собственный экземпляр класса службы, содержащий копию посредника.

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

 

Теперь обращение клиентов к веб-службе считается двухэтапным. Можно создать класс центра обслуживания и экспортировать метод GetStock() для клиентов (как пример акции в контексте). GetStock() инициирует асинхронный вызов (этап 1), чтобы неявно запустить поток. Эта процедура нового потока создает объект посредника для обращения к веб-службе (этап 2).

Следующий Листинг-5 показывает реализацию класса ServiceAsync с использованием асинхронного метода начало/конец.

// Листинг-5. Класс службы с методом начало/конец

// Определяется класс асинхронной службы
public class ServiceAsync
{
    // Определяется закрытый асинхронный метод Делегат на этапе 1
    private delegate GetStockStatus
            AsyncGetStockDelegate(ref string symbol);
   
    // Альтернативное событие, если никакая процедура обратного вызова не задана
    public event OnGetStockResult OnGetStock;

    // Посредник веб-службы
    private Service _wsProxy1;
   
    // Это открытый метод, вызываемый пользователем
    public void GetStock(string symbol, OnGetStockResult callbackProc)
    {
        // Создается закрытый асинхронный делегат.
        AsyncGetStockDelegate dlgt = new AsyncGetStockDelegate(GetStock);
       
        callbackData data = null;
        if (callbackProc !=null)
        {
            data = new callbackData();
            data._callbackProc = callbackProc;
        }

        // Инициируется асинхронный запрос.
        IAsyncResult ar = dlgt.BeginInvoke(ref symbol,
                          new AsyncCallback(AsyncGetStockResult), data);
    }

    // Это закрытая процедура потока
    private GetStockStatus GetStock(ref string symbol)
    {
        // Этап 2: Используется _wsProxy1 для обращения к веб-службе.
        // Возвращает состояние в GetStockStatus

        _wsProxy1 = new Service();
        ... ... ...
        symbol = value.ToString("F");
        return GetStockStatus.OK;
    }

    // Структура данных обратного вызова
    private class callbackData
    {
        public OnGetStockResult _callbackProc;
        // Затем идут другие данные для передачи и возврата
    }

    // Асинхронный обратный вызов, когда запрос завершен на этапе 1
    private void AsyncGetStockResult(IAsyncResult ar)
    {
        AsyncGetStockDelegate dlgt =
                (AsyncGetStockDelegate)((AsyncResult)ar).AsyncDelegate;
               
        string result = string.Empty;
        GetStockStatus status = dlgt.EndInvoke(ref result, ar);

        callbackData data = (callbackData)ar.AsyncState;
        if (data != null)
        {
            OnGetStockResult callbackProc = data._callbackProc;
            callbackProc(result, status);    // Вызывается заданный пользователем делегат
        }
        else
        if (OnGetStock != null)
            OnGetStock(result, status);      // Если нет делегата, возбуждается событие
    }
   
    public void Cancel()
    {
        _wsProxy1.Abort();
    }
}

// Определяется состояние результата
public enum GetStockStatus { OK, Exception, TimeOut, Invalid, Cancelled }

// Определяется делегат для события, чтобы возбудить результат
public delegate void OnGetStockResult(string result,
                             GetStockStatus status);

Первый открытый GetStock() принимает символ и заданный пользователем обратный вызов. Затем он создает объект AsyncGetStockDelegate, dlgt и готовит данные обратного вызова. Как только dlgt инициирует асинхронный запрос, вызывается второй закрытый GetStock() для выполнения задачи с помощью веб-службы.

Для клиента гибкость в вызове GetStock() состоит в том, что он сделан двойным образом. Посмотрите на внутренний обратный вызов AsyncGetStockResult(). Если пользователь задает процедуру обратного вызова, она используется для возврата результата. Если обратный вызов не задан, возбуждается событие OnGetStock(), чтобы известить вызывающую функцию о входящих результатах. Это можно сделать так:

ServiceAsync sa = new ServiceAsync();
sa.GetStock(symbol, new OnGetStockResult(OnGetStock));

Или аннулировать обратный вызов путем присоединения обработчика события:

as.OnGetStock += new OnGetStockResult(OnGetStock);
as.GetStock(symbol, null);

Альтернатива службе-синглтону для управления несколькими вызовами – порождать поток для каждого вызова пользователя. Листинг-6 показывает такую конструкцию.

// Листинг-6. Класс службы, порождающий поток

// Определяется класс потока службы
public class ServiceThread
{
    // Событие для отправки результата
    public event OnGetStockResult OnGetStock;
   
    // Посредник веб-службы
    private Service _wsProxy2;
    private string  _symbol;
    private int     _timeout;
   
    // Это открытый метод, вызываемый пользователем
    public void GetStock(string symbol)
    {
        _symbol = symbol;
        _wsProxy2 = new Service();
        _wsProxy2.GetStockWithTimeoutCompleted +=
            new GetStockWithTimeoutCompletedEventHandler(GetStockCompleted);
           
        Thread thread = new Thread(new ThreadStart(GetStock));
        thread.Start();
    }

    // Это закрытая процедура потока
    private void GetStock()
    {
        _wsProxy2.GetStockWithTimeoutAsync(_symbol, _timeout, _symbol);
    }

    // Обработчик события GetStockWithTimeoutCompletedEventHandler в .NET 2
    private void GetStockCompleted(Object sender,
                          GetStockWithTimeoutCompletedEventArgs gca)
    {
        // Этап 2: Используется _wsProxy для обращения к веб-службе.
        // В зависимости от результатов в gca, возбуждается событие
        ... ... ...
        OnGetStock(_symbol +" " +gca.Result.ToString("F"), GetStockStatus.OK);
    }
       
    public void Cancel()
    {
        _wsProxy2.Abort();
    }
}

// Определяется состояние результата
public enum GetStockStatus { OK, Exception, TimeOut, Invalid, Cancelled }

// Определяется делегат для события для возбуждения результата
public delegate void OnGetStockResult(string result,
                             GetStockStatus status);

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


Клиентское время простоя

В тестовой службе (Листинг-1) серверный процесс обрабатывает время простоя, переданное клиентом. Касательно посредника веб-службы, он имеет свойство Timeout (унаследованное от WebClientProtocol), предназначенное для выполнения синхронного запроса. В асинхронном режиме посредник не предоставляет время простоя напрямую, вероятно, так как можно использовать Abort() (тоже из WebClientProtocol) для отмены зависшего запроса.

Иногда в асинхронной модели приходится ждать завершения запроса, и время простоя было бы желательно, тогда как сервер и его посредник не предоставляют метод времени простоя. Поэтому клиенту приходится самому разбираться с временем простоя. Повторно вызывая BeginGetStock() при запуске запроса, он возвращает следующий объект r:

IAsyncResult r = _weProxy1.BeginGetStock(symbol, null, null);

Нельзя вызывать r.AsyncWaitHandle.WaitOne(timeout, false), так как WaitOne() не освобождает текущий поток, пока он не вернет управление, так что он блокирует отмену.

Одно решение – установить цикл для опроса свойства IsCompleted IAsyncResult, моделируя время простоя. Листинг-7 показывает данный подход с совместной проверкой на время простоя и на флаг отмены.

// Листинг-7. Опрос для достижения клиентского времени простоя (wsasyExp4.txt)

// Пример 4. Опрос для достижения клиентского времени простоя (wsasyExp4.txt)

public GetStockStatus GetStock(ref string symbol)
{
    _cancel = true;
    _weProxy1 = new Service();
    IAsyncResult r = _weProxy1.BeginGetStock(symbol, null, null);   
   
    // Опрашивается здесь, если _cancel равен истине, прерывается
    // Моделируется время простоя с интервалом 10 мс.
    int i = 0;
    int n =_timeout/10;

    for (; i < n; i++)
    {
        if (_cancel)
        {
            symbol = "<" +symbol +">";
            _weProxy1.Abort();
            return GetStockStatus.Cancelled;
        }

        if (r.IsCompleted == true)
            break;

        Thread.Sleep(10);
    }               

    if (r.IsCompleted == false && i==n)
    {
        symbol = "[" +symbol +"]";
        _weProxy1.Abort();
        return GetStockStatus.TimeOut;
    }

//    если (!r.AsyncWaitHandle.WaitOne(_timeout, true))
//        return GetStockStatus.TimeOut;

    double value;
    try
    {
        value = _weProxy1.EndGetStock(r);
    }
    catch (Exception e)
    {  
       ... ... ...
    }

    ... ... ...

    symbol = value.ToString("F");
    return GetStockStatus.OK;
}

public void Cancel()
{
    _cancel = true;
}

Используется флаг _cancel в Cancel() вместо прямого вызова _weProxy1.Abort(). При завершении цикла можно определить время простоя и прервать запрос к серверу. Как только r.IsCompleted установлен в true, вызов завершается с осмысленным значением, возвращенным из _weProxy1.EndGetStock(r).

Недостаток опроса в том, что цикл потребляет много ресурсов в виде циклов центрального процесса. Тщательно следите за этим недостатком асинхронной реализации.

Вывод

Сегодня разработка программного обеспечения переходит с ранней объектно/компонентно-ориентированной модели на модель на базе служб. Асинхронные механизмы могут широко использоваться в системе на базе служб, в веб-серверах XML, в дистанционной связи .NET и в основе связи Windows в .NET 3. В данной статье представлено несколько моделей проектирования, типичные асинхронные реализации с обратными вызовами, делегатами, событиями и потоками. Затронуты два интересных аспекта – отмена и время простоя. Каждый представленный тут подход имеет свои плюсы и минусы, которые надо учитывать на практике. Хотя примеры проектов написаны на C# в .NET 1.1 и 2.0, основные методы рекомендуются для систем с разными версиями, языками и платформами.