C#中异步调用的设计模式

时间:2011-01-03 13:33:49

标签: c# asynchronous domain-driven-design mvp

我正在设计一个具有多个层的桌面应用程序:GUI层(WinForms MVP)保存对适配器类接口的引用,这些适配器调用执行实际工作的BL类。

除了执行来自GUI的请求之外,BL还会触发GUI可以通过接口订阅的一些事件。例如,BL中的CurrentTime对象会定期更改,GUI应该反映更改。

有两个涉及多线程的问题:

  1. 我需要做一些逻辑 调用异步,以便它们不会阻止GUI。
  2. GUI接收的一些事件是从非GUI线程触发的。
  3. 处理多线程的最佳级别是什么?我的直觉说Presenter是最适合的,我是对的吗?你能给我一些做我需要的示例应用程序吗?并且主持人是否有理由持有对表单的引用,以便它可以在其上调用委托?

    编辑:除非有人给出更好的答案,否则赏金可能会归于亨利克。

4 个答案:

答案 0 :(得分:5)

我会考虑使用基于Task的BLL来表示那些可以被描述为“后台操作”的部分(也就是说,它们是由UI启动并具有明确的完成点)。 Visual Studio Async CTP包括描述基于任务的异步模式(TAP)的文档;我建议以这种方式设计BLL API(即使尚未发布async / await语言扩展)。

对于BLL的“订阅”部分(即,它们是由UI启动并无限期地继续),有几个选项(按照我个人喜好的顺序):

  1. 使用基于Task的API,但TaskCompletionSource永远不会完成(或仅在应用程序关闭时被取消而完成)。在这种情况下,我建议您编写自己的IProgress<T>EventProgress<T>(在异步CTP中):IProgress<T>为您的BLL提供报告进度的接口(替换进度事件)和{{1处理捕获EventProgress<T>以便将“报告进度”委托编组到UI线程。
  2. 使用Rx的SynchronizationContext框架;这是一个很好的设计搭配,但学习曲线相当陡峭,而且比我个人喜欢的稳定性差(这是一个预发布库)。
  3. 使用老式的基于事件的异步模式(EAP),您可以在其中捕获BLL中的IObservable并通过将事件排队到该上下文来引发事件。
  4. 编辑2011-05-17:自从编写上述内容以来,Async CTP团队已经表示不推荐方法(1)(因为它有点滥用“进度报告”系统),并且Rx团队发布了阐明其语义的文档。我现在推荐Rx用于订阅。

答案 1 :(得分:2)

这取决于您正在编写的应用程序类型 - 例如 - 您是否接受错误?您的数据要求是什么 - 软实时?酸?最终是一致的和/或部分连接的/有时是断开连接的客户?

请注意并发和异步之间存在区别。您可以使用异步,因此调用方法调用交错,而不实际执行程序。

一个想法可能是拥有应用程序的读写端,写入端在发生更改时发布事件。这可能会导致事件驱动的系统 - 读取端将根据已发布的事件构建,并且可以重建。 UI可以是任务驱动的 - 因为要执行的任务会产生BL将采用的命令(或者如果您愿意,则生成域层)。

合乎逻辑的下一步,如果你有上述内容,也是事件来源。然后,您将通过先前提交的内容重新创建写模型的内部状态。有一个关于CQRS / DDD的谷歌小组可以帮助你解决这个问题。

关于更新UI,我发现System.Reactive,System.Interactive,System.CoreEx中的IObservable接口非常适合。它允许您跳过不同的并发调用上下文 - 调度程序 - 线程池等,它与任务并行库很好地互操作。

您还必须考虑将业务逻辑放在何处 - 如果您进行域驱动我会说您可以将它放在您的应用程序中,因为您已经为您分发的二进制文件制定了更新程序,当升级的时候,但也可以选择将它放在服务器上。当面向连接的代码失败时,命令可以是执行写入端更新和方便工作单元的好方法(它们很小且可序列化,并且可以围绕它们设计UI)。

举个例子,看看this thread,使用此代码,为IObservable.ObserveOnDispatcher(...)添加优先级 - 调用:

    public static IObservable<T> ObserveOnDispatcher<T>(this IObservable<T> observable, DispatcherPriority priority)
    {
        if (observable == null)
            throw new NullReferenceException();

        return observable.ObserveOn(Dispatcher.CurrentDispatcher, priority);
    }

    public static IObservable<T> ObserveOn<T>(this IObservable<T> observable, Dispatcher dispatcher, DispatcherPriority priority)
    {
        if (observable == null)
            throw new NullReferenceException();

        if (dispatcher == null)
            throw new ArgumentNullException("dispatcher");

        return Observable.CreateWithDisposable<T>(o =>
        {
            return observable.Subscribe(
                obj => dispatcher.Invoke((Action)(() => o.OnNext(obj)), priority),
                ex => dispatcher.Invoke((Action)(() => o.OnError(ex)), priority),
                () => dispatcher.Invoke((Action)(() => o.OnCompleted()), priority));
        });
    }

上面的示例可以像blog entry讨论

一样使用
public void LoadCustomers()
{
    _customerService.GetCustomers()
        .SubscribeOn(Scheduler.NewThread)
        .ObserveOn(Scheduler.Dispatcher, DispatcherPriority.SystemIdle)
        .Subscribe(Customers.Add);
}

...例如,对于一个虚拟的星巴克商店,你会有一个类似'Barista'类的域实体,它会产生'CustomerBoughtCappuccino'事件:{cost:'$ 3',timestamp:'2011 -01-03 12:00:03.334556 GMT + 0100',......等}。您的读者会订阅这些活动。读取方可以是一些数据模型 - 对于每个呈现数据的屏幕 - 视图将具有唯一的ViewModel类 - 它将与可观察字典like this中的视图同步。存储库将是(:IObservable),您的演示者将订阅所有这些,或只是其中的一部分。那样你的GUI就可以:

  1. 任务驱动 - &gt;命令驱动BL,重点关注用户操作
  2. 异步
  3. 读写偏析
  4. 鉴于您的BL只接受命令并且不在该显示之上“对所有页面都足够好”类型的读取模型,您可以将其中的大部分内容,内部保护和私有,这意味着你可以使用System.Contracts来证明你没有任何错误(!)。它会产生您的读模型将读取的事件。您可以从Caliburn Micro中了解有关所产生的异步任务(IAsyncResults)工作流的编排的主要原则。

    您可以阅读一些Rx design guidelines。关于事件采购和cqrs的cqrsinfo.com。如果你真的有兴趣超越异步编程领域进入并发编程领域,微软已经免费发布了一个关于如何编写代码的written book井。

    希望它有所帮助。

答案 2 :(得分:1)

我会考虑“线程代理中介模式”。例如CodeProject

基本上,适配器上的所有方法调用都在工作线程上运行,所有结果都在UI线程上返回。

答案 3 :(得分:0)

推荐的方法是在GUI上使用线程,然后使用Control.Invoke()更新控件。

如果您不想在GUI应用程序中使用线程,可以使用BackgroundWorker类。

最佳做法是在表单中使用一些逻辑来从外部更新控件,通常是公共方法。当从非MainThread的线程进行此调用时,必须使用control.InvokeRequired/control.Invoke()保护非法线程访问(其中control是要更新的目标控件)。

看看这个AsynCalculatePi示例,也许这是一个很好的起点。