代码之家  ›  专栏  ›  技术社区  ›  rory.ap

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

  •  9
  • rory.ap  · 技术社区  · 6 年前

    我在用 this question


    太长,读不下去了 :如果不应该在异步包装器中包装同步代码,那么如何处理实现需要异步实现的接口方法的长时间运行的线程阻塞方法?


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

    我想让客户机保持响应性:允许UI客户机与正在运行的进程交互,我还希望线程得到有效使用,因为进程需要可伸缩性:根据队列中的工作,可以有任意数量的异步数据库或磁盘操作。因此我使用async/await "all the way"

    CancellationToken ,用“Async”命名,并返回 Task s。

    我有一个数据存储库服务,它执行CRUD操作来持久化我的域实体。假设现在,我正在使用 an API for this that doesn't natively support async . 将来,我可能会将其替换为一个同步的操作,但目前数据存储库服务将同步执行其大部分操作,其中许多操作是长时间运行的(因为API阻塞了数据库IO)。

    现在,我明白了 任务 s可以同步运行。我的服务类中实现BLL中接口的方法将如我所解释的那样同步运行,但是使用者(我的BLL、客户机等)将同步运行 它们要么是1:异步运行,要么是2:在很短的时间内同步运行。 What the methods shouldn't do is wrap synchronous code inside an async call to 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());
        }
    }
    

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

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

    1 回复  |  直到 6 年前
        1
  •  10
  •   pere57    6 年前

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

    反对将异步包装器用于同步方法的建议的主要原因如下 MSDN article

    1. 确保客户机了解任何同步库函数的真实性质
    2. 每个函数有两个版本,以避免增加库的表面积

    不管怎样 . 你实际上是在说,我选择了(2)而不考虑(1)。你已经给出了一个合理的理由-从长远来看,你知道你的同步库API将被取代。

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