Подводные камни в (Mis) использовании итераторов C # для реализации сопрограмм - PullRequest
15 голосов
/ 15 декабря 2009

Я пишу рефакторинг программы Silverlight для использования части ее существующей бизнес-логики из службы WCF. При этом я столкнулся с ограничением в Silverlight 3, которое позволяет только асинхронные вызовы к службам WCF, чтобы избежать случаев, когда длительные или неотвечающие вызовы служб блокируют поток пользовательского интерфейса (SL имеет интересную модель очередей для вызова служб WCF). в потоке пользовательского интерфейса).

Как следствие, написание того, что когда-то было простым, становится все более сложным ( см. Примеры кода в конце моего вопроса ).

В идеале я бы использовал сопрограммы , чтобы упростить реализацию, но, к сожалению, C # в настоящее время не поддерживает сопрограммы в качестве средства родного языка. Однако в C # есть концепция генераторов (итераторов), использующих синтаксис yield return. Моя идея состоит в том, чтобы переопределить ключевое слово yield, чтобы позволить мне построить простую модель сопрограммы для той же логики.

Однако я неохотно делаю это, потому что я обеспокоен тем, что могут быть некоторые скрытые (технические) ловушки, которых я не ожидаю (учитывая мою относительную неопытность с Silverlight и WCF). Я также обеспокоен тем, что механизм реализации может быть неясен будущим разработчикам и может препятствовать, а не упрощать их усилия по поддержке или расширению кода в будущем. Я видел этот вопрос на SO о повторном назначении итераторов для построения конечных автоматов: реализация конечного автомата с использованием ключевого слова yield "1013 *, и хотя это не совсем то же самое, что я делаю, оно делает заставь меня задуматься.

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

Исходная версия кода, отличная от WCF, выглядит примерно так:

void Button_Clicked( object sender, EventArgs e ) {
   using( var bizLogic = new BusinessLogicLayer() ) {
       try  {
           var resultFoo = bizLogic.Foo();
           // ... do something with resultFoo and the UI
           var resultBar = bizLogic.Bar(resultFoo);
           // ... do something with resultBar and the UI
           var resultBaz = bizLogic.Baz(resultBar);
           // ... do something with resultFoo, resultBar, resultBaz
       }
   }
}

Пересмотренная версия WCF становится немного более сложной (даже без обработки исключений и до / после тестирования условий):

// fields needed to manage distributed/async state
private FooResponse m_ResultFoo;  
private BarResponse m_ResultBar;
private BazResponse m_ResultBaz;
private SomeServiceClient m_Service;

void Button_Clicked( object sender, EventArgs e ) {
    this.IsEnabled = false; // disable the UI while processing async WECF call chain
    m_Service = new SomeServiceClient();
    m_Service.FooCompleted += OnFooCompleted;
    m_Service.BeginFoo();
}

// called asynchronously by SL when service responds
void OnFooCompleted( FooResponse fr ) {
    m_ResultFoo = fr.Response;
    // do some UI processing with resultFoo
    m_Service.BarCompleted += OnBarCompleted;
    m_Service.BeginBar();
}

void OnBarCompleted( BarResponse br ) {
    m_ResultBar = br.Response;
    // do some processing with resultBar
    m_Service.BazCompleted += OnBazCompleted;
    m_Service.BeginBaz();
} 

void OnBazCompleted( BazResponse bz ) {
    m_ResultBaz = bz.Response;
    // ... do some processing with Foo/Bar/Baz results
    m_Service.Dispose();
}

Приведенный выше код, очевидно, является упрощением, так как в нем исключаются обработка исключений, проверки недействительности и другие методы, которые могут потребоваться в производственном коде. Тем не менее, я думаю, что это демонстрирует быстрое увеличение сложности, которое начинает происходить с моделью асинхронного программирования WCF в Silverlight. Рефакторинг исходной реализации (которая не использовала сервисный уровень, а скорее имела свою логику, встроенную в клиент SL) быстро превращается в сложную задачу. И тот, который может быть весьма подвержен ошибкам.

Совместная версия кода будет выглядеть примерно так (я еще не проверял это):

void Button_Clicked( object sender, EventArgs e ) {
    PerformSteps( ButtonClickCoRoutine );
}

private IEnumerable<Action> ButtonClickCoRoutine() {
    using( var service = new SomeServiceClient() ) {
        FooResponse resultFoo;
        BarResponse resultBar;
        BazResponse resultBaz;

        yield return () => {
            service.FooCompleted = r => NextStep( r, out resultFoo );
            service.BeginFoo();
        };
        yield return () => {
            // do some UI stuff with resultFoo
            service.BarCompleted = r => NextStep( r, out resultBar );
            service.BeginBar();
        };
        yield return () => {
            // do some UI stuff with resultBar
            service.BazCompleted = r => NextStep( r, out resultBaz );
            service.BeginBaz();
        };
        yield return () => {
            // do some processing with resultFoo, resultBar, resultBaz
        }
    }
}

private void NextStep<T>( T result, out T store ) { 
    store = result;
    PerformSteps();  // continues iterating steps
}

private IEnumerable<Action> m_StepsToPerform;
private void PerformSteps( IEnumerable<Action> steps ) {
   m_StepsToPerform = steps;
   PerformSteps();        
}

private void PerformSteps() {
   if( m_StepsToPerform == null ) 
       return; // nothing to do

   m_StepsToPerform.MoveNext();
   var nextStep = m_StepsToPerform.Current;
   if( nextStep == null ) {
       m_StepsToPerform.Dispose();
       m_StepsToPerform = null;
       return; // end of steps
   }
   nextStep();
}

В вышеприведенном коде есть множество вещей, которые необходимо улучшить. Но основная предпосылка заключается в выделении шаблона продолжения (создание точки перехвата для обработки исключений и различных проверок), в то же время позволяя основанной на событиях асинхронной модели WCF двигаться, когда выполняется каждый шаг - в основном, когда завершается последний асинхронный вызов WCF. Хотя на первый взгляд это выглядит как дополнительный код, стоит отметить, что PerformSteps() и NextStep() можно использовать повторно, только реализация в ButtonClickCoRoutine() будет меняться с каждым отдельным сайтом реализации.

Я не совсем уверен, что мне нравится эта модель, и я бы не удивился, если бы существовал более простой способ ее реализации. Но я не смог найти ни одного на «межсетях» или MSDN, или где-либо еще. Заранее спасибо за помощь.

Ответы [ 4 ]

11 голосов
/ 15 декабря 2009

Вам определенно следует взглянуть на Параллельное и координационное время выполнения . Именно для этой цели используются итераторы.

С другой стороны, вы также должны взглянуть на Параллельные расширения и его подход к продолжениям. Parallel Extensions является частью .NET 4.0, тогда как CCR требует отдельного лицензирования. Я бы посоветовал бы вам использовать рамки, написанные людьми, которые едят, дышат и спят. Просто слишком легко ошибиться в деталях.

4 голосов
/ 15 декабря 2009

Реактивные расширения для .NET предоставляют гораздо более чистую модель для этого.

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

1 голос
/ 15 декабря 2009

Я не читал всю вашу вещь.

Они используют эту стратегию в робототехнической студии CCR, и ряд других проектов используют эту стратегию. Альтернативой является использование LINQ, см., Например, этот блог для описания. Реактивная структура (Rx) как бы построена по этим направлениям.

Лука упоминает в своем PDC talk , что, возможно, будущая версия C # / VB может добавить асинхронные примитивы к языку.

Между тем, если вы можете использовать F #, это выигрышная стратегия. Прямо сейчас то, что вы можете сделать с F #, выдувает все остальное прямо из воды.

EDIT

Чтобы процитировать пример из моего блога, предположим, что у вас есть клиент WCF, для которого вы хотите вызвать несколько методов. Синхронная версия может быть записана как

// a sample client function that runs synchronously 
let SumSquares (client : IMyClientContract) = 
    (box client :?> IClientChannel).Open() 
    let sq1 = client.Square(3) 
    let sq2 = client.Square(4) 
    (box client :?> IClientChannel).Close() 
    sq1 + sq2 

и соответствующий асинхронный код будет

// async version of our sample client - does not hold threads 
// while calling out to network 
let SumSquaresAsync (client : IMyClientContract) = 
    async { do! (box client :?> IClientChannel).OpenAsync() 
            let! sq1 = client.SquareAsync(3) 
            let! sq2 = client.SquareAsync(4) 
            do! (box client :?> IClientChannel).CloseAsync() 
            return sq1 + sq2 } 

Нет сумасшедших обратных вызовов, вы можете использовать управляющие конструкции, такие как if-then-else, while, try-finally и т. Д., Напишите его почти так же, как вы пишете прямой код, и все работает, но теперь это асинхронно. Очень просто взять заданную пару методов BeginFoo / EndFoo и создать соответствующие асинхронные методы F # для использования в этой модели.

0 голосов
/ 15 декабря 2009

Вы также можете рассмотреть AsyncEnumerator Джеффри Рихтера, который является частью его библиотеки 'power threading'. Он работал вместе с командой CCR для разработки CCR. AsyncEnumerator, по словам Джеффри, более «легкий», чем CCR. Лично я играл с AsyncEnumerator, но не с CCR.

Я не использовал его в гневе - до сих пор я нашел ограничения с использованием перечислителей для реализации сопрограмм слишком болезненными. В настоящее время изучаю F # из-за асинхронных рабочих процессов (если я правильно помню имя), которые выглядят как полноценные сопрограммы или «продолжения» (я забыл правильное имя или точное различие между терминами).

В любом случае, вот несколько ссылок:

http://www.wintellect.com/PowerThreading.aspx

Видео 9 канала на AsyncEnumerator

MSDN Статья

...