Длительная синхронная реализация интерфейса, возвращающего задачу - PullRequest
0 голосов
/ 27 августа 2018

Я использую этот вопрос в качестве основы для моего вопроса.


TL; DR : если вы не должны переносить синхронный код в асинхронную оболочку, как вы справляетесь с долговременными методами блокировки потоков, которые реализуют интерфейсный метод, который ожидает асинхронный реализация?


Допустим, у меня есть приложение, которое работает непрерывно для обработки рабочей очереди. Это серверное приложение (работающее в основном без присмотра), но в нем есть пользовательский интерфейс для более детального контроля поведения приложения в соответствии с требованиями бизнес-процессов: запуск, остановка, настройка параметров во время выполнения, получение прогресса и т. д.

Существует слой бизнес-логики, в который сервисы внедряются как зависимости.
BLL определяет набор интерфейсов для этих служб.

Я хочу, чтобы клиент реагировал: позволить клиенту пользовательского интерфейса взаимодействовать с запущенным процессом, а также хочу, чтобы потоки использовались эффективно, потому что процесс должен быть масштабируемым: может быть любое количество асинхронных операций с базой данных или диском в зависимости от на работу в очереди. Таким образом, я использую async / await "полностью" .

Для этого у меня есть методы в сервисных интерфейсах, которые, очевидно, предназначены для поощрения async / await и поддержки отмены, потому что они принимают CancellationToken, имеют имя "Async" и возвращают Task s.

У меня есть служба хранилища данных, которая выполняет операции CRUD для сохранения сущностей моего домена. Допустим, в настоящее время я использую API для этого, который изначально не поддерживает async . В будущем я могу заменить его на тот, который работает, но в настоящее время служба хранилища данных выполняет большинство своих операций синхронно, многие из них являются длительными операциями (поскольку API блокирует ввод-вывод базы данных).

Теперь я понимаю, что методы, возвращающие Task s, могут работать синхронно. Методы в моем классе обслуживания, которые реализуют интерфейсы в моем 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());
    }
}

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 Ответ

0 голосов
/ 28 августа 2018

Вы не выставляете асинхронные оболочки для синхронных методов. Вы не автор внешней библиотеки, вы клиент. Как клиент, вы адаптируете API библиотеки к интерфейсу вашего сервиса.

Основные причины, по которым рекомендуется использовать асинхронные оболочки для синхронных методов (см. Статью MSDN , упомянутую в вопросе):

  1. чтобы клиент знал об истинной природе любой синхронной библиотечной функции
  2. , чтобы дать клиенту контроль над тем, как вызывать функцию (асинхронная или синхронная).
  3. , чтобы избежать увеличения площади поверхности библиотеки, имея 2 версии каждой функции

Что касается интерфейса службы, определяя только асинхронные методы, которые вы выбираете для асинхронного вызова операций библиотеки независимо от того, что . Вы фактически говорите, я сделал свой выбор для (2) независимо от (1). И вы дали разумную причину - в течение длительного времени вы знаете, что ваш API-интерфейс библиотеки синхронизации будет заменен.

В качестве дополнительного аспекта, даже несмотря на то, что функции API вашей внешней библиотеки являются синхронными, они не имеют длительной привязки к процессору. Как вы сказали, они блокируют IO. Они на самом деле связаны с IO. Они просто блокируют поток, ожидающий ввода-вывода, а не освобождают его.

...