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

发布于 2024-10-10 10:21:36 字数 450 浏览 0 评论 0原文

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

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

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

  1. 我需要做一些逻辑 异步调用,这样它们就不会阻塞 GUI。
  2. GUI 接收到的一些事件是从非 GUI 线程触发的。

在什么级别上最好处理多线程?我的直觉告诉我 Presenter 是最合适的,对吗?您能给我一些满足我需要的示例应用程序吗?演示者持有对表单的引用以便调用表单上的委托是否有意义?

编辑: 赏金可能会交给 Henrik,除非有人给出更好的答案。

I'm designing a desktop application with multiple layers: the GUI layer (WinForms MVP) holds references to interfaces of adapter classes, and these adapters call BL classes that do the actual work.

Apart from executing requests from the GUI, the BL also fires some events that the GUI can subscribe to through the interfaces. For example, there's a CurrentTime object in the BL that changes periodically and the GUI should reflect the changes.

There are two issues that involve multithreading:

  1. I need to make some of the logic
    calls asynchronous so that they don't block the GUI.
  2. Some of the events the GUI recevies are fired from non-GUI threads.

At what level is it best to handle the multithreading? My intuition says that the Presenter is the most suitable for that, am I right? Can you give me some example application that does what I need? And does it make sense for the presenter to hold a reference to the form so it can invoke delegates on it?

EDIT: The bounty will probably go to Henrik, unless someone gives an even better answer.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

半窗疏影 2024-10-17 10:21:36

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

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

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

编辑 2011-05-17: 自编写上述内容以来,Async CTP 团队已声明不推荐方法 (1)(因为它在某种程度上滥用了“进度报告”系统),并且 Rx 团队已经发布了澄清其语义的文档。我现在推荐 Rx 进行订阅。

I would look at using a Task-based BLL for those parts that can be described as "background operations" (that is, they're started by the UI and have a definite completion point). The Visual Studio Async CTP includes a document describing the Task-based Asynchronous Pattern (TAP); I recommend designing your BLL API in this way (even though the async/await language extensions haven't been released yet).

For parts of your BLL that are "subscriptions" (that is, they're started by the UI and continue indefinitely), there are a few options (in order of my personal preference):

  1. Use a Task-based API but with a TaskCompletionSource that never completes (or only completes by being cancelled as part of application shutdown). In this case, I recommend writing your own IProgress<T> and EventProgress<T> (in the Async CTP): the IProgress<T> gives your BLL an interface for reporting progress (replacing progress events) and EventProgress<T> handles capturing the SynchronizationContext for marshalling the "report progress" delegate to the UI thread.
  2. Use Rx's IObservable framework; this is a good match design-wise but has a fairly steep learning curve and is less stable than I personally like (it's a pre-release library).
  3. Use the old-fashioned Event-based Asynchronous Pattern (EAP), where you capture the SynchronizationContext in your BLL and raise events by queuing them to that context.

EDIT 2011-05-17: Since writing the above, the Async CTP team has stated that approach (1) is not recommended (since it somewhat abuses the "progress reporting" system), and the Rx team has released documentation that clarifies their semantics. I now recommend Rx for subscriptions.

笔落惊风雨 2024-10-17 10:21:36

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

请注意,并发性和异步性之间存在区别。您可以具有异步性,从而调用方法调用交错,而无需实际同时执行程序。

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

如果您具备上述条件,那么合乎逻辑的下一步就是采用事件溯源。然后,您将通过之前提交的内容重新创建写入模型的内部状态。有一个关于 CQRS/DDD 的 google 小组可以帮助您解决此问题。

关于更新 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', timestamp : '2011-01-03 12:00:03.334556 GMT+0100', ... 等 }。您的读取端将订阅这些事件。读取端可以是某种数据模型——对于每个显示数据的屏幕——视图将有一个唯一的 ViewModel 类——它将与可观察字典中的视图同步 像这样。存储库将是 (:IObservable),并且您的演示者将订阅全部内容,或仅订阅其中的一部分。这样你的 GUI 就可以是:

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

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

有一些Rx 设计指南 你可以阅读。以及 cqrsinfo.com 有关事件溯源和 cqrs 的信息。如果您确实有兴趣超越异步编程领域进入并发编程领域,Microsoft 已经发布了一个很好的 免费编写的书,介绍如何编写此类代码。

希望有帮助。

It depends on what type of application you are writing - for example - do you accept bugs? What are your data requirements - soft realtime? acid? eventually consistent and/or partially connected/sometimes disconnected clients?

Beware that there's a distinction between concurrency and asynchronocity. You can have asynchronocity and hence call method call interleaving without actually having a concurrently executing program.

One idea could be to have a read and write side of your application, where the write-side publishes events when it's been changed. This could lead to an event driven system -- the read side would be built from the published events, and could be rebuilt. The UI could be task-driven - in that a task to perform would produce a command that your BL would take (or domain layer if you so wish).

A logical next step, if you have the above, is to also go event-sourced. Then you would recreate internal state of the write-model through what has been previously committed. There's a google group about CQRS/DDD that could help you with this.

With regards to updating the UI, I've found that the IObservable interfaces in System.Reactive, System.Interactive, System.CoreEx are well suited. It allows you to skip around different concurrent invocation contexts - dispatcher - thread pool, etc, and it interops well with the Task Parallel Library.

You'd also have to consider where you put your business logic -- if you go domain driven I'd say you could put it in your application as you'd have an updating procedure in place for the binaries you distribute anyway, when time comes to upgrade, but there's also the choice of putting it on the server. Commands could be a nice way to perform the updates to the write-side and a convenient unit of work when connection-oriented code fails (they are small and serializable and the UI can be designed around them).

To give you an example, have a look at this thread, with this code, that adds a priority to the IObservable.ObserveOnDispatcher(...)-call:

    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));
        });
    }

The example above could be used like this blog entry discusses

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

... So for example with a virtual starbucks shop, you'd have a domain entity that has something like a 'Barista' class, which produces events 'CustomerBoughtCappuccino' : { cost : '$3', timestamp : '2011-01-03 12:00:03.334556 GMT+0100', ... etc }. Your read-side would subscribe to these events. The read side could be some data model -- for each of your screens that present data -- the view would have a unique ViewModel-class -- which would be synchronized with the view in an observable dictionary like this. The repository would be (:IObservable), and your presenters would subscribe to all of that, or just a part of it. That way your GUI could be:

  1. Task driven -> command driven BL, with focus on user operations
  2. Async
  3. Read-write-segregated

Given that your BL only takes commands and doesn't on top of that display a 'good enough for all pages'-type of read-model, you can make most things in it internal, internal protected and private, meaning you can use System.Contracts to prove that you don't have any bugs in it (!). It would produce events that your read-model would read. You could take the main principles from Caliburn Micro about the orchestration of workflows of yielded asynchronous tasks (IAsyncResults).

There are some Rx design guidelines you could read. And cqrsinfo.com about event sourcing and cqrs. If you are indeed interested in going beyond the async programming sphere into the concurrent programming sphere, Microsoft has released a well written book for free, on how to program such code.

Hope it helps.

暮倦 2024-10-17 10:21:36

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

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

I would consider the "Thread Proxy Mediator Pattern". Example here on CodeProject

Basically all method calls on your Adaptors run on a worker thread and all results are returned on the UI thread.

喜爱皱眉﹌ 2024-10-17 10:21:36

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

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

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

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

The recommended way is using threads on the GUI, and then update your controls with Control.Invoke().

If you don't want to use threads in your GUI application, you can use the BackgroundWorker class.

The best practice is having some logic in your Forms to update your controls from outside, normally a public method. When this call is made from a thread that is not the MainThread, you must protect illegal thread accesses using control.InvokeRequired/control.Invoke() (where control is the target control to update).

Take a look to this AsynCalculatePi example, maybe it's a good starting point.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文