提高流式传输大型(1-10 gb)文件的速度 .Net Core

Increase Speed for Streaming Large(1-10 gb) files .Net Core

我正在尝试使用 multipartform-data 通过我的 API 上传 *.iso 文件并将它们流式传输到本地文件夹。 我使用 Stream.CopyAsync(destinationStream) 并且它运行缓慢,但还不错。但现在我需要报告进展情况。所以我使用了自定义 CopyTOAsync 并向其添加了进度报告。但是该方法非常慢(完全不能接受),甚至与 Stream::CopyToASync.

相比
 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::CopyToAsync 的默认缓冲区大小是 81920 字节,我首先使用了相同的值,然后我尝试将缓冲区大小增加到 104857600 字节——没有区别。

对于如何提高自定义 CopyToAsync 的性能,您还有其他想法吗?

  • 始终使用 ConfigureAwaitawait 来为异步延续指定线程同步。
    • 根据平台,省略 ConfigureAwait 可能默认与 UI 线程(WPF、WinForms)或任何线程(ASP.NET Core)同步。如果它与 Stream 复制操作中的 UI 线程同步,那么性能急剧下降也就不足为奇了。
    • 如果您是 运行 线程同步上下文中的代码,那么您的 await 语句将被不必要地延迟,因为程序将继续安排到一个可能很忙的线程。
  • 使用大小至少为几百 KiB 的缓冲区 - 甚至是兆字节大小的缓冲区用于异步操作 - 而不是典型的 4KiB 或 80KiB 大小的数组。
    • This QA shows benchmarks that demonstrate that significantly larger buffers are necessary for async IO to have better performance than synchronous IO.
  • 如果你使用 FileStream 确保你使用 FileOptions.AsynchronoususeAsync: true 否则 FileStream 伪造 它的异步通过使用线程池线程而不是 Windows' 原生异步 IO 来执行阻塞 IO 的操作。

关于您的实际代码 - 只需使用 Stream::CopyToAsync 而不是您自己重新实现它。如果您想要进度报告,请考虑子类化 Stream(作为代理包装器)。

以下是我编写代码的方式:

  1. First, add my ProxyStream class from this 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 回调目标内部(我假设它与 UI 线程同步) - in which case a better solution is to use a (System.Threading.Channels.Channel) 用于 ProgressProxyStream(或什至您的 IProgress<T> 的实现)将进度报告转储到不阻塞任何其他 IO activity.