(Mis)使用C#迭代器实现协同程序的缺陷

时间:2009-12-14 21:11:38

标签: c# wcf silverlight-3.0 asynchronous yield

我正在编写重构Silverlight程序,以便从WCF服务中消耗其现有业务逻辑的一部分。在这样做时,我遇到了Silverlight 3中的限制,它只允许对WCF服务的异步调用,以避免长时间运行或无响应的服务调用阻塞UI线程(SL有一个有趣的排队模型来调用WCF服务)在UI线程上。)

因此,编写曾经简单明了的内容变得越来越复杂(请参阅我的问题末尾的代码示例)。

理想情况下,我会使用coroutines来简化实现,但遗憾的是,C#目前不支持协同程序作为本机语言工具。但是,C#确实具有使用yield return语法的生成器(迭代器)的概念。我的想法是重新使用yield关键字,以允许我为相同的逻辑构建一个简单的协同模型。

但是,我不愿意这样做,因为我担心可能会有一些我不期待的隐藏(技术)陷阱(鉴于我对Silverlight和WCF的相对缺乏经验)。我也担心未来的开发人员可能不清楚实现机制,并且可能会阻碍而不是简化他们将来维护或扩展代码的努力。我已经看到这个问题关于重新设计迭代器以构建状态机:implementing a state machine using the "yield" keyword,虽然它与我正在做的事情不完全相同,但它确实让我停下来。

但是,我需要做一些事情来隐藏服务调用的复杂性,并管理此类更改中的缺陷的工作量和潜在风险。我对可以用来解决这个问题的其他想法或方法持开放态度。

代码的原始非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();
}

上述代码显然是一种简化,因为它省略了异常处理,无效检查以及生产代码中必需的其他实践。尽管如此,我认为它证明了Silverlight中异步WCF编程模型开始出现的复杂性的快速增长。重新分解原始实现(不使用服务层,而是将其逻辑嵌入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()中的实现会随着每个不同的实现网站而改变。

我不完全确定我喜欢这个模型,如果有更简单的方法来实现它,我也不会感到惊讶。但我无法在“interwebs”或MSDN或其他任何地方找到一个。在此先感谢您的帮助。

4 个答案:

答案 0 :(得分:11)

你一定要看Concurrency and Coordination Runtime。它使用迭代器来达到这个目的。

另一方面,您还应该看看Parallel Extensions及其延续方法。 Parallel Extensions是.NET 4.0的一部分,而CCR需要单独的许可。我建议你选择那些吃饭,呼吸和睡觉的人写的框架。很容易让你自己弄错。

答案 1 :(得分:4)

Reactive Extensions for .NET为处理此问题提供了更清晰的模型。

它们提供了扩展,使您可以以更清晰的方式编写针对异步事件的简单委托。我建议调查它们,并使它们适应这种情况。

答案 2 :(得分:1)

我没看完你的全部内容。

他们在CCR机器人工作室中使用此策略,许多其他项目使用此策略。另一种方法是使用LINQ,参见例如this blog有关说明。 Reactive框架(Rx)有点建立在这些方面。

Luca在他的PDC talk中提到可能未来版本的C#/ VB可能会向该语言添加异步原语。

与此同时,如果您可以使用F#,那么这是一个成功的策略。现在你可以用F#做什么,这样就可以把所有其他东西从水中吹走。

修改

引用我博客中的示例,假设您有一个要调用几个方法的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这样的控制结构,而try-finally等等,写它几乎就像你编写直线代码一样,一切正常,但现在它是异步的。获取给定的BeginFoo / EndFoo方法并使用相应的F#异步方法非常容易在此模型中使用。

答案 3 :(得分:0)

您可能还想考虑Jeffrey Richter的AsyncEnumerator,它是“强力线程”库的一部分。他与CCR团队合作开发CCR。根据Jeffrey的说法,AsyncEnumerator比CCR更“轻量级”。就我个人而言,我曾经使用过AsyncEnumerator而不是使用CCR。

我没有在愤怒中使用它 - 到目前为止,我发现使用枚举器来实现协程的限制太痛苦了。目前正在学习F#,因为除了其他事项的异步工作流程(如果我记得正确的名称),看起来他们是完全成熟的协程或'延续'(我忘记了正确的名称或术语之间的确切区别)。

无论如何,这里有一些链接:

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

Channel 9 video on AsyncEnumerator

MSDN Article