Увеличение скорости потоковой передачи больших (1-10 ГБ) файлов. Net Core - PullRequest
1 голос
/ 20 марта 2020

Я пытаюсь загрузить файлы * .iso через мой API, используя multipartform-data, и передать их в локальную папку. Я использовал Stream.CopyAsyn c (destinationStream), и он работал медленно, но не так уж плохо. Но теперь мне нужно сообщить о прогрессе. Поэтому я использовал пользовательский CopyTOAsyn c и добавил в него отчет о проделанной работе. Но метод очень медленный (вообще не приемлем), даже по сравнению с Stream :: CopyToASyn c.

 public async Task CopyToAsync(Stream source, Stream destination, long? contentLength, ICommandContext context, int bufferSize = 81920 )
    {
        var buffer = new byte[bufferSize];
        int bytesRead;
        long totalRead = 0;
        while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length)) > 0)
        {
            await destination.WriteAsync(buffer, 0, bytesRead);
            totalRead += bytesRead;
            context.Report(CommandResources.RID_IMAGE_LOADIND, Math.Clamp((uint)((totalRead * 100) / contentLength), 3, 99));
        }
        _logger.Info($"Total read during upload : {totalRead}");
    }

Что я пробовал: размер буфера по умолчанию для Stream :: CopyToAsyn c равен 81920 байт, сначала я использовал то же значение, затем попытался увеличить размер буфера до 104857600 байт - без разницы.

Есть ли у вас другие идеи о том, как улучшить производительность пользовательского CopyToAsyn c?

1 Ответ

2 голосов
/ 20 марта 2020
  • Всегда используйте ConfigureAwait с await, чтобы указать синхронизацию потоков для продолжения asyn c.
    • В зависимости от платформы, пропуск ConfigureAwait может по умолчанию выполнять синхронизацию с потоком пользовательского интерфейса (WPF, WinForms) или с любым потоком (ASP. NET Core). Если он синхронизируется с потоком пользовательского интерфейса внутри вашей операции потокового копирования, то неудивительно, что производительность занимает пикирование.
    • Если вы запускаете код в контексте с синхронизацией потоков, то ваши операторы await будут неоправданно задерживается, потому что программа планирует продолжение в потоке, который мог бы быть занят в противном случае.
  • Использовать буфер размером не менее пары сотен КиБ или даже мегабайтный буфер для asyn c операции - не типичный массив размером 4 КБ или 80 КБ.
  • Если вы используете FileStream, убедитесь, что вы использовали FileOptions.Asynchronous или useAsync: true, в противном случае FileStream будет подделывать свои асинхронные c операции, выполняя блокировку ввода-вывода с использованием потока пула потоков вместо Windows 'native asyn c IO.

Что касается вашего фактического кода - просто используйте Stream::CopyToAsync вместо того, чтобы переопределять его самостоятельно. Если вы хотите получать отчеты о проделанной работе, рассмотрите вместо этого подкласс Stream (в качестве прокси-оболочки).

Вот как я бы написал ваш код:

  1. Сначала добавьте мой ProxyStream класс из этого GitHub Gist для вашего проекта.
  2. Затем подкласс ProxyStream для добавления поддержки IProgress:
  3. Убедитесь, что все экземпляры FileStream созданы с FileOptions.Asynchronous | FileOptions.SequentialScan.
  4. Использование CopyToAsync.
public class ProgressProxyStream : ProxyStream
{
    private readonly IProgress<(Int64 soFar, Int64? total)> progress;
    private readonly Int64? total;

    public ProgressProxyStream( Stream stream, IProgress<Int64> progress, Boolean leaveOpen )
        : base( stream, leaveOpen ) 
    {
        this.progress = progress ?? throw new ArgumentNullException(nameof(progress));
        this.total = stream.CanSeek ? stream.Length : (Int64?)null;
    }

    public override Task<Int32> ReadAsync( Byte[] buffer, Int32 offset, Int32 count, CancellationToken cancellationToken )
    {
        this.progress.Report( ( offset, this.total ) );
        return this.Stream.ReadAsync( buffer, offset, count, cancellationToken );
    }
}

Если производительность все еще страдает с вышеуказанным ProgressProxyStream, тогда я готов поспорить, что узкое место находится внутри IProgress.Report цель обратного вызова (которая, как я полагаю, синхронизируется с потоком пользовательского интерфейса) - , и в этом случае лучшим решением будет использование (System.Threading.Channels.Channel) для ProgressProxyStream (или даже вашей реализации IProgress<T>) для создания отчетов о ходе выполнения без блокирования других операций ввода-вывода.

...