繁体   English   中英

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

[英]Design pattern for asynchronous calls in C#

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

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

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

  1. 我需要使一些逻辑调用异步,以便它们不会阻止GUI。
  2. GUI接收的一些事件是从非GUI线程触发的。

处理多线程的最佳级别是什么? 我的直觉说Presenter是最适合的,我是对的吗? 你能给我一些做我需要的示例应用程序吗? 并且主持人是否有理由持有对表单的引用,以便它可以调用它上面的代理?

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

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

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

  1. 使用基于Task的API但使用永远不会完成的TaskCompletionSource (或仅在应用程序关闭时被取消完成)。 在这种情况下,我建议编写自己的IProgress<T>EventProgress<T> (在Async CTP中): IProgress<T>为BLL提供一个用于报告进度(替换进度事件)和EventProgress<T>处理捕获的接口SynchronizationContext用于将“report progress”委托编组到UI线程。
  2. 使用Rx的IObservable框架; 这是一个很好的设计搭配,但学习曲线相当陡峭,并且比我个人喜欢的稳定性差(它是一个预发布库)。
  3. 使用老式的基于事件的异步模式(EAP),您可以在其中捕获BLL中的SynchronizationContext ,并通过将事件排入该上下文来引发事件。

编辑2011-05-17:自编写上述内容以来,Async CTP团队已经表示不建议使用方法(1)(因为它有点滥用“进度报告”系统),并且Rx团队发布了明确其语义的文档。 我现在推荐Rx用于订阅。

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

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

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

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

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

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

给你举个例子,看看这个线程 ,使用此代码,增加了一个优先考虑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));
        });
    }

上面的示例可以像博客条目讨论的那样使用

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

...例如,对于一个虚拟的星巴克商店,你有一个域实体,它有类似'Barista'类,它产生'CustomerBoughtCappuccino'事件:{cost:'$ 3',时间戳:'2011-01- 03 12:00:03.334556 GMT + 0100',......等}。 您的读者会订阅这些活动。 读取方面可能会有一些数据模型-为每个屏幕,目前的数据-视图将有一个独特的视图模型级-这将在可观察到的字典中的视图中同步这样 存储库将是(:IObservable),您的演示者将订阅所有这些,或只是其中的一部分。 这样你的GUI可能是:

  1. 任务驱动 - >命令驱动BL,重点关注用户操作
  2. 异步
  3. 读写偏析

鉴于你的BL只接受命令并且不在该显示之上“足够好用于所有页面”类型的读取模型,你可以将其中的大部分内容,内部保护和私有,这意味着你可以使用系统.Contracts证明你没有任何错误(!)。 它会产生您的读模型将读取的事件。 您可以从Caliburn Micro获取有关生成异步任务(IAsyncResults)工作流的编排的主要原则。

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

希望能帮助到你。

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

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM