【问题标题】:Pitfalls of (Mis)Using C# Iterators to Implement Coroutines(错误)使用 C# 迭代器实现协程的陷阱
【发布时间】:2010-12-26 14:06:56
【问题描述】:

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

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

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

但是,我不愿意这样做,因为我担心可能存在一些我没有预料到的隐藏(技术)陷阱(鉴于我对 Silverlight 和 WCF 的经验相对缺乏)。我还担心未来的开发人员可能不清楚实现机制,并且可能会阻碍而不是简化他们未来维护或扩展代码的努力。我在 SO 上看到了这个关于重新利用迭代器来构建状态机的问题: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 或其他任何地方找到一个。提前感谢您的帮助。

【问题讨论】:

  • 没有好的答案,只是说我们感受到了你的痛苦。我们正在对人们如何在 Silverlight 和其他框架中使用异步进行大量研究。我们为使迭代器看起来像廉价协程而进行的各种转换只是一个开始,但没有足够的通用性来解决这个领域的所有问题。我肯定希望看到这种模式在语言中清晰地表达,就像我们清晰地表达迭代器模式的生成(yield)和消耗(foreach)方面一样。
  • 我猜这个问题解释了将 async/await 添加到 C# 的动机。
  • 我对@9​​87654323@ 使用IAsyncEnumerable、IAsyncEnumerator、await foreach 等的看法。

标签: c# wcf silverlight-3.0 asynchronous yield


【解决方案1】:

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

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

【讨论】:

  • 我同意。我的理解是 CCR 有一定的许可限制(如果我理解正确,我将无法使用)。 Parallel Extensions 库和 .NET 4.0 目前处于预览阶段——我已经查看了它们,但我没有看到任何类或工具可以做这种事情。你有没有想过 PTE 的某个特定部分?
  • LBushkin:您可以将并行扩展作为响应式扩展的一部分进行反向移植(有关链接,请参阅我的帖子)。有一个 Task.FromAsync 方法(+overloads),可让您从基于 IAsyncResult 的方法对构造任务,以及使用 Task.ContinueWith 进行延续。
  • 里德:谢谢。我会看看那个。
  • 作为新发布的 Robotics studio R3 的一部分,CCR 现在可以免费下载
【解决方案2】:

Reactive Extensions for .NET 提供了一个更简洁的模型来处理这个问题。

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

【讨论】:

  • 我查看了 Rx.NET,但它本身处于预览阶段,可能不适用于我的时间范围。我也不清楚究竟如何编写这种类型的代码 RX.NET - 我已经看到说明将事件源视为 IObservable 的示例 - 本质上是无限迭代器,但不知道如何构造这种模式。你知道我可以看的任何例子吗?
  • 看看这个:themechanicalbride.blogspot.com/2009/07/… 需要一个小的包装库,但很容易通过延续来扩展它。然而,这里有很多使用 Rx 从 observables 制作任务的示例。
  • +1 RX 是在 Silverlight 中采取此类事情的方向。我不应该太担心 Rx 的“预览”状态,如果这真的是一个问题,也许整个 Silverlight 不适合你。
【解决方案3】:

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

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

【讨论】:

  • 在回复 Reed 和 Jon 时,我查看了 CCR、Parallel Extensions 和 Reactive.NET - 但是我还没有看到一个很好的例子来说明我正在做的事情.这将大大有助于我决定哪些(如果有的话)这些库可以满足要求。
  • 你有我可以看的 F# 示例吗?我不认为我可以在 Silverlight 3 应用程序中使用 F#,但我可以从它使用的方法中学到一些东西。
  • 在此处查看 Luca 2008 年的 PDC 演讲:channel9.msdn.com/pdc2008/TL11 在视频中,从 50 分钟开始观看大约 10 分钟,他展示了在 F# 中进行异步调用是多么容易。您可以在 Silverlight 3 应用程序中使用 F# 库。
【解决方案4】:

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

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

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

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

Channel 9 video on AsyncEnumerator

MSDN Article

【讨论】:

    猜你喜欢
    • 2022-01-06
    • 1970-01-01
    • 2014-12-13
    • 2017-05-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多