提高流式传输大型(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 的性能,您还有其他想法吗?
- 始终使用
ConfigureAwait
和 await
来为异步延续指定线程同步。
- 根据平台,省略
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.Asynchronous
或 useAsync: true
否则 FileStream
将 伪造 它的异步通过使用线程池线程而不是 Windows' 原生异步 IO 来执行阻塞 IO 的操作。
关于您的实际代码 - 只需使用 Stream::CopyToAsync
而不是您自己重新实现它。如果您想要进度报告,请考虑子类化 Stream
(作为代理包装器)。
以下是我编写代码的方式:
- First, add my
ProxyStream
class from this GitHub Gist 到您的项目。
- 然后继承
ProxyStream
以添加对 IProgress
的支持:
- 确保
FileStream
个实例是使用 FileOptions.Asynchronous | FileOptions.SequentialScan
创建的。
- 使用
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.
我正在尝试使用 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 的性能,您还有其他想法吗?
- 始终使用
ConfigureAwait
和await
来为异步延续指定线程同步。- 根据平台,省略
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.Asynchronous
或useAsync: true
否则FileStream
将 伪造 它的异步通过使用线程池线程而不是 Windows' 原生异步 IO 来执行阻塞 IO 的操作。
关于您的实际代码 - 只需使用 Stream::CopyToAsync
而不是您自己重新实现它。如果您想要进度报告,请考虑子类化 Stream
(作为代理包装器)。
以下是我编写代码的方式:
- First, add my
ProxyStream
class from this GitHub Gist 到您的项目。 - 然后继承
ProxyStream
以添加对IProgress
的支持: - 确保
FileStream
个实例是使用FileOptions.Asynchronous | FileOptions.SequentialScan
创建的。 - 使用
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.