Рефакторинг библиотеки для асинхронизации, как я могу избежать повторения?

У меня есть такой способ:

    public void Encrypt(IFile file)
    {
        if (file == null)
            throw new ArgumentNullException(nameof(file));

        string tempFilename = GetFilename(file);
        using (var stream = new FileStream(tempFilename, FileMode.OpenOrCreate))
        {
            this.EncryptToStream(file, stream);
            file.Write(stream);
        }

        File.Delete(tempFilename);
    }

Однако я хочу написать другой метод, очень похожий, но вместо этого он вызывает WriteAsync, например:

    public async Task EncryptAsync(IFile file)
    {
        if (file == null)
            throw new ArgumentNullException(nameof(file));

        string tempFilename = GetFilename(file);
        using (var stream = new FileStream(tempFilename, FileMode.OpenOrCreate))
        {
            this.EncryptToStream(file, stream);
            await file.WriteAsync(stream);
        }

        File.Delete(tempFilename);
    }

Однако мне не нравится иметь два метода с практически дублирующимся кодом. Как я могу избежать этого? Правильный подход кажется, что я должен использовать Action/Delegate, но подписи разные.

Мысли?

Ответ 1

Однако мне не нравится иметь два метода с практически дублирующимся кодом. Как я могу избежать этого?

Существует несколько подходов, описанных в моей статье MSDN, посвященной разработке асинхронного браузера.

1) Сделайте асинхронные API-интерфейсы только асинхронными.

Это самое решительное решение (с точки зрения обратной совместимости), но с технической точки зрения вы можете утверждать, что это наиболее правильно. При таком подходе вы замените Encrypt на EncryptAsync.

Хотя это лучший подход к ИМО, конечные пользователи могут не согласиться.:)

2) Примените взломать блокировки в асинхронной версии.

public void Encrypt(IFile file)
{
  EncryptAsync(file).GetAwaiter().GetResult();
}

Обратите внимание, что (как я описываю в своем блоге) чтобы избежать взаимоблокировок, вам нужно использовать ConfigureAwait(false) всюду в вашей асинхронной версии.

Недостатки этого взлома:

  • Вам буквально приходится использовать ConfigureAwait(false) для каждого await в вашей версии async и каждом методе, который он вызывает. Забудьте еще одного, и у вас есть возможность взаимоблокировки. Обратите внимание, что некоторые библиотеки не используют ConfigureAwait(false) везде для всех платформ (в частности, HttpClient на мобильных платформах).

3) Примените hack к запуску асинхронной версии в потоке пула потоков и блокировке этого.

public void Encrypt(IFile file)
{
  Task.Run(() => EncryptAsync(file)).GetAwaiter().GetResult();
}

Этот подход полностью исключает ситуацию взаимоблокировки, но имеет свои недостатки:

  • Асинхронный код должен быть запущен в потоке пула потоков.
  • Асинхронный код может выполняться в нескольких потоках пулов потоков на протяжении всего его существования. Если асинхронный код неявно зависит от синхронизации однопоточного контекста или если он использует нить-локальное состояние, тогда этот подход не будет работать без какой-либо перезаписи.

4) Передайте аргумент флага.

Это подход, который мне нравится лучше всего, если вы не хотите принимать подход (1).

private async Task EncryptAsync(IFile file, bool sync)
{
  if (file == null)
    throw new ArgumentNullException(nameof(file));

  string tempFilename = GetFilename(file);
  using (var stream = new FileStream(tempFilename, FileMode.OpenOrCreate))
  {
    this.EncryptToStream(file, stream);
    if (sync)
      file.Write(stream);
    else
      await file.WriteAsync(stream);
  }

  File.Delete(tempFilename);
}

public void Encrypt(IFile file)
{
  EncryptAsync(file, sync: true).GetAwaiter().GetResult();
}

public Task EncryptAsync(IFile file)
{
  return EncryptAsync(file, sync: false);
}

Булевский флаг, безусловно, уродливый - и красный флаг для правильного дизайна ООП, но он скрыт в методе private и не так опасен, как другие хаки.

В моей статье рассматривается несколько других хаков (однопоточный поток пула потоков и вложенные петли сообщений), но я вообще не рекомендую их.


На стороне примечания, если ваш код действительно использует FileStream, вам нужно явно открыть его для асинхронного доступа для получения истинных асинхронных операций. То есть вы должны вызвать конструктор либо передать true для параметра isAsync, либо установить флаг FileOptions.Asynchronous в значение для параметра options.

Ответ 2

Что-то вроде этого:

public async Task EncryptAsync(IFile file, bool AAsync)
{
    if (file == null)
        throw new ArgumentNullException(nameof(file));

    string tempFilename = GetFilename(file);
    using (var stream = new FileStream(tempFilename, FileMode.OpenOrCreate))
    {
        this.EncryptToStream(file, stream);
        if (AAsync)
            await file.WriteAsync(stream);
        else
            file.Write(stream);
    }

    File.Delete(tempFilename);
}