返回 Task 的接口的长时间运行的同步实现



我用这个问题作为我问题的基础。


TL;DR:如果你不应该将同步代码包装在异步包装器中,你如何处理长时间运行的线程阻塞方法,这些方法实现了需要异步实现的接口方法?


假设我有一个连续运行以处理工作队列的应用程序。它是一个服务器端应用程序(大部分无人值守运行(,但它有一个 UI 客户端,可以根据业务流程的要求对应用程序的行为进行更精细的控制:启动、停止、在执行期间调整参数、获取进度等。

有一个业务逻辑层,服务作为依赖项注入其中。
BLL 为这些服务定义了一组接口。

我想保持客户端的响应:允许 UI 客户端与正在运行的进程进行交互,我还希望有效地使用线程,因为进程需要可扩展:根据队列中的工作,可能会有任意数量的异步数据库或磁盘操作。因此,我"一路"使用异步/等待。

为此,我在服务接口中有一些方法,这些方法显然旨在鼓励 async/await 并支持取消,因为它们采用CancellationToken,以"Async"命名,并返回Tasks。

我有一个数据存储库服务,该服务执行 CRUD 操作以保留我的域实体。假设目前,我正在使用一个本身不支持异步的 API。将来,我可能会将其替换为这样做的,但目前数据存储库服务会同步执行其大部分操作,其中许多是长时间运行的操作(因为 API 会阻止数据库 IO(。

现在,我知道返回Task的方法可以同步运行。我的服务类中实现 BLL 中接口的方法将同步运行,如我所解释的那样,但使用者(我的 BLL、客户端等(将假定它们是 1:异步运行或 2:同步运行很短的时间。这些方法不应该做的是将同步代码包装在异步调用中以Task.Run

我知道我可以在界面中定义同步和异步方法。
在这种情况下,我不想这样做,因为我正在尝试使用异步"一路"语义,并且因为我没有编写供客户使用的 API;如上所述,我不想稍后将我的 BLL 代码从使用同步版本更改为使用异步版本。

下面是数据服务接口:

public interface IDataRepository
{
Task<IReadOnlyCollection<Widget>> 
GetAllWidgetsAsync(CancellationToken cancellationToken);
}

它的实现:

public sealed class DataRepository : IDataRepository
{
public Task<IReadOnlyCollection<Widget>> GetAllWidgetsAsync(
CancellationToken cancellationToken)
{
/******* The idea is that this will 
/******* all be replaced hopefully soon by an ORM tool. */
var ret = new List<Widget>();
// use synchronous API to load records from DB
var ds = Api.GetSqlServerDataSet(
"SELECT ID, Name, Description FROM Widgets", DataResources.ConnectionString);
foreach (DataRow row in ds.Tables[0].Rows)
{
cancellationToken.ThrowIfCancellationRequested();
// build a widget for the row, add to return.  
}
// simulate long-running CPU-bound operation.
DateTime start = DateTime.Now;
while (DateTime.Now.Subtract(start).TotalSeconds < 10) { }
return Task.FromResult((IReadOnlyCollection<Widget>) ret.AsReadOnly());
}
}

布莱尔:

public sealed class WorkRunner
{
private readonly IDataRepository _dataRepository;
public WorkRunner(IDataRepository dataRepository) => _dataRepository = dataRepository;
public async Task RunAsync(CancellationToken cancellationToken)
{
var allWidgets = await _dataRepository
.GetAllWidgetsAsync(cancellationToken).ConfigureAwait(false);
// I'm using Task.Run here because I want this on 
// another thread even if the above runs synchronously.
await Task.Run(async () =>
{
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
foreach (var widget in allWidgets) { /* do something */ }
await Task.Delay(2000, cancellationToken); // wait some arbitrary time.
}
}).ConfigureAwait(false);
}
}

演示和演示逻辑:

private async void HandleStartStopButtonClick(object sender, EventArgs e)
{
if (!_isRunning)
{
await DoStart();
}
else
{
DoStop();
}
}
private async Task DoStart()
{
_isRunning = true;          
var runner = new WorkRunner(_dependencyContainer.Resolve<IDataRepository>());
_cancellationTokenSource = new CancellationTokenSource();
try
{
_startStopButton.Text = "Stop";
_resultsTextBox.Clear();
await runner.RunAsync(_cancellationTokenSource.Token);
// set results info in UI (invoking on UI thread).
}
catch (OperationCanceledException)
{
_resultsTextBox.Text = "Canceled early.";
}
catch (Exception ex)
{
_resultsTextBox.Text = ex.ToString();
}
finally
{
_startStopButton.Text = "Start";
}
}
private void DoStop()
{
_cancellationTokenSource.Cancel();
_isRunning = false;
}

所以问题是:如何处理长时间运行的阻塞方法,这些方法实现了需要异步实现的接口方法?这是最好打破"同步代码没有异步包装器"规则的示例吗?

您不会公开同步方法的异步包装器。您不是外部库的作者,而是客户。作为客户端,您正在使库 API 适应您的服务接口。

建议反对对同步方法使用异步包装器的主要原因是(总结自问题中引用的 MSDN 文章(:

  1. 确保客户端了解任何同步库函数的真实性质
  2. 使客户端能够控制如何调用函数(异步或同步(。
  3. 避免通过每个函数有 2 个版本来增加库的表面积

对于服务接口,通过仅定义异步方法,您可以选择无论如何异步调用库操作。你实际上是在说,我已经做出了(2(的选择,而不管(1(。你已经给出了一个合理的理由 - 从长远来看,你知道你的同步库 API 将被替换。

附带一点,即使外部库 API 函数是同步的,它们也不会长时间运行 CPU 绑定。正如您所说,它们阻止了IO。它们实际上是 IO 绑定的。他们只是阻止线程等待 IO,而不是释放它。

最新更新