我正在设计一个具有多个层的桌面应用程序:GUI层(WinForms MVP)保存对适配器类接口的引用,这些适配器调用执行实际工作的BL类。
除了执行来自GUI的请求之外,BL还会触发GUI可以通过接口订阅的一些事件。例如,BL中的CurrentTime
对象会定期更改,GUI应该反映更改。
有两个涉及多线程的问题:
处理多线程的最佳级别是什么?我的直觉说Presenter是最适合的,我是对的吗?你能给我一些做我需要的示例应用程序吗?并且主持人是否有理由持有对表单的引用,以便它可以在其上调用委托?
编辑:除非有人给出更好的答案,否则赏金可能会归于亨利克。
答案 0 :(得分:5)
我会考虑使用基于Task
的BLL来表示那些可以被描述为“后台操作”的部分(也就是说,它们是由UI启动并具有明确的完成点)。 Visual Studio Async CTP包括描述基于任务的异步模式(TAP)的文档;我建议以这种方式设计BLL API(即使尚未发布async
/ await
语言扩展)。
对于BLL的“订阅”部分(即,它们是由UI启动并无限期地继续),有几个选项(按照我个人喜好的顺序):
Task
的API,但TaskCompletionSource
永远不会完成(或仅在应用程序关闭时被取消而完成)。在这种情况下,我建议您编写自己的IProgress<T>
和EventProgress<T>
(在异步CTP中):IProgress<T>
为您的BLL提供报告进度的接口(替换进度事件)和{{1处理捕获EventProgress<T>
以便将“报告进度”委托编组到UI线程。SynchronizationContext
框架;这是一个很好的设计搭配,但学习曲线相当陡峭,而且比我个人喜欢的稳定性差(这是一个预发布库)。IObservable
并通过将事件排队到该上下文来引发事件。编辑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就可以:
鉴于您的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示例,也许这是一个很好的起点。