16

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

因此,编写曾经很简单的东西,正变得越来越复杂(请参阅我问题末尾的代码示例)。

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

但是,我不愿意这样做,因为我担心可能存在一些我没有预料到的隐藏(技术)陷阱(鉴于我对 Silverlight 和 WCF 的经验相对缺乏)。我还担心未来的开发人员可能不清楚实现机制,并且可能会阻碍而不是简化他们未来维护或扩展代码的努力。我在 SO 上看到过这个关于重新使用迭代器来构建状态机的问题:使用“yield”关键字实现状态机,虽然这与我正在做的事情不完全相同,但它确实让我停下来。

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

代码的原始非 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

4 回答 4

11

您绝对应该查看Concurrency and Coordination Runtime。它使用迭代器正是为了这个目的。

另一方面,您还应该查看并行扩展及其延续方法。Parallel Extensions 是 .NET 4.0 的一部分,而 CCR 需要单独的许可。我建议您使用由吃、呼吸和睡觉这些东西的人编写的框架。自己弄错细节太容易了。

于 2009-12-14T21:17:33.923 回答
4

.NET的响应式扩展提供了一个更简洁的模型来处理这个问题。

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

于 2009-12-14T21:18:02.207 回答
1

我没有读完你的全部内容。

他们在 CCR 机器人工作室中使用了这种策略,其他一些项目也使用了这种策略。另一种方法是使用 LINQ,请参阅此博客的说明。Reactive 框架 (Rx) 就是按照这些思路构建的。

Luca 在他的PDC 演讲中提到,未来版本的 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、while、try-finally 等控制结构,几乎就像编写直线代码一样编写它,一切正常,但现在它是异步的。采用给定的 BeginFoo/EndFoo 方法对并制作相应的 F# 异步方法以用于此模型非常容易。

于 2009-12-14T21:18:14.997 回答
0

您可能还想考虑 Jeffrey Richter 的 AsyncEnumerator,它是他的“强力线程”库的一部分。他与 CCR 团队一起开发了 CCR。根据 Jeffrey 的说法,AsyncEnumerator 比 CCR 更“轻量级”。就我个人而言,我玩过 AsyncEnumerator,但没有玩过 CCR。

不过,我还没有愤怒地使用它——到目前为止,我发现使用枚举器实现协程的局限性太痛苦了。目前正在学习 F#,因为异步工作流(如果我没记错的话)看起来像是完整的协程或“延续”(我忘记了正确的名称或术语之间的确切区别)。

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

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

AsyncEnumerator 上的第 9 频道视频

MSDN 文章

于 2009-12-15T00:53:11.370 回答