返回任务的接口的长期同步实现

时间:2018-08-27 16:57:25

标签: c# asynchronous interface async-await synchronous

我使用this question作为问题的依据。


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


假设我有一个连续运行的应用程序来处理工作队列。它是一个服务器端应用程序(通常在无人值守的情况下运行),但是它具有UI客户端,可以根据业务流程的需要对应用程序的行为进行更细粒度的控制:启动,停止,在执行过程中调整参数,获取进度等

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

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

为此,我在服务接口中有一些方法显然旨在鼓励异步/等待并支持取消,因为它们采用CancellationToken,以“ Async”命名,并返回{{1} } s。

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

现在,我知道返回Task的方法可以同步运行。服务类中实现BLL中接口的方法将按照我的解释同步运行,但是使用者(我的BLL,客户端等)将假定:1:异步运行或2:在很短的时间内同步运行。 What the methods shouldn't do is wrap synchronous code inside an async call to Task.Run

我知道我可以在界面中同时定义同步和异步方法。
在这种情况下,由于我要采用异步的“所有方式”语义,并且因为我没有编写供客户使用的API,所以我不想这样做。如上所述,我不想以后将BLL代码从使用同步版本更改为使用异步版本。

这是数据服务界面:

Task

它的实现:

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

BLL:

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

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

1 个答案:

答案 0 :(得分:10)

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

不建议将异步包装用于同步方法的主要原因(从问题中引用的MSDN article总结):

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

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

另一方面,即使您的外部库API函数是同步的,它们也不是长时间运行的CPU绑定。如您所说,它们阻塞了IO。它们实际上是受IO约束的。他们只是阻塞等待IO的线程,而不是释放它。