• Microsoft .NET
  • WPF и Silverlight
  • Синхронные вызовы веб-службы в Silverlight: Развенчание мифа об исключительной асинхронности

Синхронные вызовы веб-службы в Silverlight: Развенчание мифа об исключительной асинхронности - Резюме Silverlight WCF

ОГЛАВЛЕНИЕ

Резюме Silverlight WCF

Еще недавно в Silverlight 1.1 синхронные вызовы веб-служб действительно поддерживались. Visual Studio генерировал синхронные методы в посредниках канала и интерфейсах. Но был один подвох: любой вызов веб-службы мог происходить только в потоке пользовательского интерфейса; иначе следовало InvalidOperationException. В Silverlight 2 RTW генерируемые посредники больше не содержат методы для синхронного использования служб. Однако сейчас можно вызвать веб-службу из любого потока.

И что происходит здесь? Веб-службы Silverlight 2 все еще имеют привязку к потоку пользовательского интерфейса, просто сейчас происходит внутренняя обработка. Когда делается вызов веб-службы, он помещается в очередь на выполнение потоком пользовательского интерфейса.

Рассмотрим, как делаются вызовы веб-службы в Silverlight 2. Сначала генерируется посредник путем создания ссылки на службу. Затем производится подписка на событие [MethodName]Completed, как показано в следующем фрагменте.

var client = new SimpleServiceClient();
client.GetGreetingCompleted += ((sender, e) => { DisplayGreeting(e.Result); });
client.GetGreetingAsync("SimpleServiceClient");

Когда делается вызов client.GetGreetingAsync, он ставится в очередь для потока пользовательского интерфейса. Даже если вызов делается из потока пользовательского интерфейса, он все же ставится в очередь. При завершении вызова вызывается обработчик GetGreetingCompleted в том же потоке, в котором был сделан вызов GetGreetingAsync.

Превращение асинхронных вызовов в синхронные

Все это прекрасно, но что если разрабатывается набор несовместимых и модульных библиотек классов с контрактами, написанными без учета асинхронного выполнения, и где ожидаются возвращаемые значения? Это серьезное ограничение может заставить нарушить видимость реализации путем требования, чтобы потребители знали, что они не должны ожидать получения возвращаемого значения, потому что может произойти вызов веб-службы. Возможность явно выполнять синхронные вызовы службы полезна. Ее убрали не для предотвращения блокировок браузера, а для обеспечения кроссбраузерной поддержки, поскольку Silverlight должен реализовывать модель плагина NPAPI(интерфейс прикладного программирования плагинов Netscape).

Первые предположения

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

(Не используйте этот код!)

var resetEvent = new ManualResetEvent(false);
var client = new SimpleServiceClient();
client.GetGreetingCompleted += ((sender, e) =>                                               
{
         DisplayGreeting(e.Result);
        resetEvent.Set();
         });
client.GetGreetingAsync("SimpleServiceClient");
resetEvent.WaitOne(); /* Это будет блокировать бессрочно. */

К несчастью, это ничего не дает. Причина состоит в том, что вызов службы должен инициироваться в потоке пользовательского интерфейса. При вызове resetEvent.WaitOne() блокируется поток пользовательского интерфейса, а значит, вызова службы вообще не происходит. Почему? В фоновом режиме вызов client.GetGreetingAsync ставится в очередь сообщений и будет выполнен, только когда поток не выполняет код пользователя. При блокировке потока вызова службы вообще не происходит.

Для дальнейшей демонстрации этого момента был слегка изменен предыдущий пример:

var client = new SimpleServiceClient();
client.GetGreetingAsync("SimpleServiceClient");
/* Ждать секунду, чтобы показать, что вызов не происходит потом. */
Thread.Sleep(1000);
/* Подписка на событие в потоке пользовательского интерфейса после вызова службы все еще работает! */
client.GetGreetingCompleted += ((sender, e) => { DisplayGreeting(e.Result); });

Заметьте, что подписка на событие GetGreetingCompleted происходит после фактического вызова GetGreetingAsync. Однако результат совпадает с предыдущим; запоздалая подписка не препятствует вызову обработчика GetGreetingCompleted.

Здесь вызов веб-службы ставится в очередь. Ничего не происходит, пока поток пользовательского интерфейса не получит возможность вызвать его. Вызов веб-службы Begin[Method] сразу же возвращает управление, но нижележащий вызов WCF не происходит потом, как показано на следующей схеме.

Рисунок: Очередь сообщений пользовательского интерфейса

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

Итак, каково решение? Нельзя вызвать службу синхронно из потока пользовательского интерфейса. Но можно вызвать службу синхронно из потока, не относящегося к пользовательскому интерфейсу, с помощью ChannelFactory, как показывает следующий фрагмент.

ThreadPool.QueueUserWorkItem(delegate
{
    var channelFactory = new ChannelFactory<ISimpleService>("*");
    var simpleService = channelFactory.CreateChannel();
    var asyncResult = simpleService.BeginGetGreeting("Daniel", null, null);
    string greeting = null;
    try
    {
        greeting = simpleService.EndGetGreeting(asyncResult);
    }
    catch (Exception ex)
    {
        DisplayMessage(string.Format(
    "Не могу соединиться с сервером. {0} {1}",
            ex.Message, ex.StackTrace));
    }
    DisplayGreeting(greeting);
});

ChannelFactory создает динамический посредник, который можно использовать для ожидания результата метода службы в потоке, не относящемся к пользовательскому интерфейсу.

Не используйте данный прием в потоке пользовательского интерфейса, иначе приложение Silverlight зависнет. К несчастью, System.ServiceModel.ChannelFactory не проверяет, выполняется ли он в потоке пользовательского интерфейса, поэтому приходится придумывать средство самостоятельно.

Вводится SynchronousChannelBroker.

SynchronousChannelBroker – написанный класс, соединяющий все шаги и делающий выполнение синхронных вызовов веб-службы простым и безопасным.
Для облегчения настройки динамических каналов службы было создано несколько делегатов BeginAction следующего вида:

public delegate IAsyncResult BeginAction<TActionArgument1>(
TActionArgument1 argument1, AsyncCallback asyncResult, object state);

Сродни обобщенному Funcs, они позволяет легко использовать посредник службы, без необходимости дорабатывать или заменять функциональность добавления ссылки на службу Visual Studio.

Почему нельзя создать 'SynchronousChannelFactory', использующий сигнатуры методов, имеющиеся в контракте службы? Можно, но это была бы трудная задача. Генератор кода Visual Studio вырабатывает интерфейсы, использующие асинхронный шаблон "Begin[MethodName]", "End[MethodName]", поэтому, чтобы сделать это, пришлось бы выкинуть много инфраструктуры. Можно написать интерфейсы службы вручную, но недостаток в том, что это введет еще больше кода для обслуживания. Данный сценарий не входит в рамки статьи.

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

var simpleService = ChannelManager.Instance.GetChannel<ISimpleService>();
string result = string.Empty;
try
{
    /* Выполняется синхронный вызов WCF. */
    result = SynchronousChannelBroker.PerformAction<string, string>(
        simpleService.BeginGetGreeting, simpleService.EndGetGreeting, "there");
}
catch (Exception ex)
{
    DisplayMessage(string.Format("Не могу соединиться с сервером. {0} {1}",
        ex.Message, ex.StackTrace));
}
DisplayGreeting(result);

Конечно, все это строго типизировано, и обобщенные типы должны соответствовать типам параметров в методах интерфейса службы.

В примере выше канал службы ISimpleService возвращается из ChannelManager. Если этот тип службы возвращен впервые, то новый канал будет создан и закеширован, иначе будет возвращен кешированный канал. После возврата канала SynchronousChannelBroker используется для вызова методов Begin и End и для возврата результата. Следующая схема дает общее представление о том, что происходит внутри.

Рисунок: Блок-схема синхронного вызова веб-службы

PerformAction из SynchronousChannelBroker имеет разные перегрузки, чтобы соответствовать перегрузкам из интерфейсов службы. В предыдущем примере PerformAction выглядит так:

public static TReturn PerformAction<TReturn, TActionArgument1>(
    BeginAction<TActionArgument1> beginAction,
    EndAction<TReturn> endAction, TActionArgument1 argument1)
{
    EnsureNonUIThread();
    var beginResult = beginAction(argument1, null, null);
    var result = endAction(beginResult);
    return result;
}

Чтобы не дать вызову службу произойти в потоке пользовательского интерфейса, вызывается EnsureUIThread(). Этот метод генерирует исключение, если текущий поток является потоком пользовательского интерфейса. Для этого он использует UISynchronizationContext.

Три главных нужных класса показаны на следующей схеме.

Рисунок: Диаграмма классов для главных классов проекта.

UISynchronizationContext

Включенный UISynchronizationContext использует RootVisual приложений (обычно Page(страница)) для получения Dispatcher(диспетчер) потока пользовательского интерфейса. Из Dispatcher можно создать DispatcherSynchronizationContext, позволяющий выполнять синхронные действия в потоке пользовательского интерфейса, а также асинхронные действия.

Иногда надо использовать UISynchronizationContext раньше назначения визуального корня. Для этого можно использовать различные перегрузки Initialize, чтобы назначить контекст диспетчеру пользовательского интерфейса. Пример этого показан в конструкторе класса Page в проекте SilverlightExamples.

UISynchronizationContext.Instance.Initialize(Dispatcher);

UISynchronizationContext использует DispatcherSynchronizationContext, инициализированный с помощью Dispatcher, и позволяет синхронно вызывать методы в потоке пользовательского интерфейса. Заметьте, что Dispatcher позволяет только вызывать асинхронно с помощью BeginInvoke.