【发布时间】:2017-07-07 04:20:05
【问题描述】:
场景
在基于 MVVMLight 的应用程序中,我使用了几个视图和服务来计算工作时间。其中包括以下服务...
- DataService:顾名思义,用于从数据库中获取数据。因为它本质上是 I/O 操作,所以这些方法使用 async/await。
- 合同:这是一个 CPU 密集型服务,实际计算工作时间(例如,对于给定的一天、一周、一个月……)
现在,让我们看看下面的方法(位于 Contract 服务中):
public TimeSpan GetWorkTimeForDay(Date date)
{
// Check if that result was already calculated before and is still
// valid. In this case, load it and return it
if (ContractCalculationCacheResults.Contains(date))
return ContractCalculationCacheResults.Get(date);
var finalWorkTime = TimeSpan.Zero;
// Get the work day from the database in a synchronous fashion
var workDay = DataService.GetWorkDay(date);
// if there is a work day in the database then finally calculate the work time
if (workDay != null)
finalWorkTime = GetWorkTimeForDay(workDay);
// cache the result
ContractCalculationCacheResults.Put(date, finalWorkTime);
// return the final result
return finalWorkTime;
}
此方法位于Contract 服务中。它依赖于DataService 来获取计算所需的数据。
如您所见,DataService.GetWorkDay() 目前是同步的。这就是数据库在相当长一段时间内被访问的方式。现在,在将应用程序从 WP8 Silverlight 移植到 UWP 时,我想清理代码并更改数据服务,以便它只提供 async 方法而不是同步方法。但现在我正在努力结合两个实际上独立的服务,其中一个是同步的,而另一个是异步的。
问题
我需要更新方法 GetWorkTimeForDay()(以及许多其他看起来相似的方法)以使用新的 DataService.GetWorkDayAsync() 方法,顾名思义,该方法的定义如下:
public async Task<WorkDay> GetWorkDayAsync(Date dateKey)
但是,这会导致许多悬而未决的问题,因为 GetWorkTimeForDay 也应该突然变成 async 方法,尽管它的本质是受 CPU 限制。所以这没有意义。
可能的解决方案
1。将 GetWorkTimeForDay 设为异步方法
结果会是这样的:
public async Task<TimeSpan> GetWorkTimeForDay(Date date)
{
...
// Get the work day from the database in a synchronous fashion
var workDay = await DataService.GetWorkDayAsync(date);
...
}
虽然这看起来很简单,但它对其余代码有很多影响。仅仅因为在任何地方都调用了GetWorkTimeForDay,所以我必须更改代码,因为这个方法现在是async。我必须为它await,因此可能还需要更改调用方法......等等。但有时不需要异步调用GetWorkTimeForDay(),因为它已经在单独的线程或后台任务中运行。更不用说迄今为止存在的所有单元测试了。所以在某些情况下它不会阻塞 UI。
此外,CPU 绑定计算突然变得异步,这不是它应该如何工作的(至少如果我正确理解 Stephen Cleary 的 this 帖子(和其他帖子)的话。
2。分离服务
所以我想到的下一个解决方案是分离服务。换句话说,不要让一项服务依赖于另一项服务。这将更改GetWorkTimeForDay() 的签名,如下所示:
public TimeSpan GetWorkTimeForDay(WorkDay workDay, Date date)
{
var finalWorkTime = TimeSpan.Zero;
if (workDay != null)
finalWorkTime = GetWorkTimeForDay(workDay);
return finalWorkTime;
}
然后调用方法将如下所示:
public async void Caller(Date date)
{
...
if (ContractCalculationCacheResults.Contains(date))
return ContractCalculationCacheResults.Get(date);
var workDay = await DataService.GetWorkDay(date);
var workTime = Contract.GetWorkTimeForDay(workDay, date);
ContractCalculationCacheResults.Put(date, workTime);
...
}
与其他解决方案一样,缺点是所有调用者方法都必须更新(及其调用者,...),即使有时这不是必需的。还会添加大量用于缓存和数据库访问的冗余代码。
从 MVVM 的角度来看,另一个缺点可能是突然将数据提供给合同服务。因此,出于充分的理由,我将不再使用或多或少违反规则的 DI。 (对我来说,问题是如何正确地将服务注入服务)
最后但同样重要的是,我还必须从 GetWorkTimeForDay 方法中删除缓存。只有在之前没有缓存结果的情况下,我才会要求数据库获取工作日并在之后进行计算。
3。 ?
...
实际问题
在基于 MVVM 的项目中将async 服务与非async 服务组合在一起的好设计是什么?
我想保持实际计算同步,因为它们受 CPU 限制。我想保持数据访问异步,因为它们不受 CPU 限制,但可能需要一些时间。
所以这篇文章不是关于如何等待await/Task/... 的结果的技术解决方案。这更像是一个关于 MVVM、服务注入其他服务、同步和异步服务组合的架构问题。
【问题讨论】:
-
对我来说,这更像是一个架构问题,而不是如何等待
async方法完成的答案。 -
Furthermore, suddenly the CPU bound calculations become asynchronous which is not how it should work (at least if I understood this post (and others) by Stephen Cleary correctly.这只是一般建议。如果您需要您的方法是异步的,那么它是否受 IO 或 CPU 限制都没有关系。对于 UI 应用程序,您真的不应该在 UI 线程上进行任何冗长的计算(无论是 IO 还是 CPU 密集型)。使用异步方法是正确的做法 -
另外,建议是“您不应该仅仅因为 CPU 绑定操作而使方法异步”。这是因为计算将完全占用一个线程,因此使用当前线程比卸载到另一个线程更有效(假设这不是 UI 线程)。但是在您的示例中,您的方法是异步的,因为
GetWorkDayAsync,所以是因为 IO 绑定操作。这里真的没有问题,只要让GetWorkTimeForDay异步 -
如果你想调用一个从同步方法返回值的异步方法并且不阻塞当前线程只需msdn.microsoft.com/en-us/library/dd270696(v=vs.110).aspx
标签: c# mvvm async-await mvvm-light