Long 运行 同步实现一个接口,即returns一个Task

Long running synchronous implementation of an interface that returns a Task

我使用 作为我的问题的基础。


TL;DR:如果你不应该在异步包装器中包装同步代码,你如何处理 long-运行ning, thread-实现期望异步实现的接口方法的阻塞方法?


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

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

我想让客户端保持响应:允许UI客户端与运行ning进程交互,我也希望线程能被高效使用,因为进程需要可伸缩:有可以是任意数量的异步数据库或磁盘操作,具体取决于队列中的工作。因此,我正在使用 async/await "all the way".

为此,我在服务接口中有明显旨在鼓励 async/await 和支持取消的方法,因为它们采用 CancellationToken,并以 "Async" 命名,和 return Tasks.

我有一个数据存储库服务,可以执行 CRUD 操作来保存我的域实体。假设目前我正在使用 an API for this that doesn't natively support async。将来,我可能会用一个可以替换它,但目前数据存储库服务同步执行其大部分操作,其中许多是 long-运行ning 操作(因为 API数据库 IO 上的块)。

现在,我明白 returning Task 方法可以 运行 同步。我的服务 class 中实现 BLL 接口的方法将 运行 同步,正如我所解释的,但消费者(我的 BLL、客户端等)将 假定 它们是 1:运行ning 异步或 2:运行ning 在很短的时间内同步。 What the methods shouldn't do is wrap synchronous code inside an async call to Task.Run.

我知道我可以在界面中定义同步和异步方法。
在这种情况下,我不想这样做,因为我正在尝试使用异步 "all the way" 语义,而且我不是在编写 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());
    }
}

BLL:

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

所以问题是:您如何处理长运行宁、阻塞方法,这些方法实现了需要异步实现的接口方法?这是一个最好打破 "no async wrapper for sync code" 规则的例子吗?

您没有公开同步方法的异步包装器。您不是外部库的作者,您是客户。作为客户端,您正在 调整 库 API 到您的服务接口。

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

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

关于您的服务接口,通过仅定义异步方法,您选择异步调用库操作无论如何。你实际上是在说,不管 (1) 是什么,我都选择了 (2)。并且您给出了合理的理由 - 从长远来看,您知道您的同步库 API 将被替换。

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