文件流.与Read()相比,ReadAsync非常慢
本文关键字:ReadAsync 非常 相比 Read 文件 | 更新日期: 2023-09-27 18:05:58
我用下面的代码来循环遍历一个文件并一次读取1024字节。第一次迭代使用FileStream.Read()
,第二次迭代使用FileStream.ReadAsync()
。
private async void Button_Click(object sender, RoutedEventArgs e)
{
await Task.Run(() => Test()).ConfigureAwait(false);
}
private async Task Test()
{
Stopwatch sw = new Stopwatch();
sw.Start();
int readSize;
int blockSize = 1024;
byte[] data = new byte[blockSize];
string theFile = @"C:'test.mp4";
long totalRead = 0;
using (FileStream fs = new FileStream(theFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
readSize = fs.Read(data, 0, blockSize);
while (readSize > 0)
{
totalRead += readSize;
readSize = fs.Read(data, 0, blockSize);
}
}
sw.Stop();
Console.WriteLine($"Read() Took {sw.ElapsedMilliseconds}ms and totalRead: {totalRead}");
sw.Reset();
sw.Start();
totalRead = 0;
using (FileStream fs = new FileStream(theFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, (blockSize*2), FileOptions.Asynchronous | FileOptions.SequentialScan))
{
readSize = await fs.ReadAsync(data, 0, blockSize).ConfigureAwait(false);
while (readSize > 0)
{
totalRead += readSize;
readSize = await fs.ReadAsync(data, 0, blockSize).ConfigureAwait(false);
}
}
sw.Stop();
Console.WriteLine($"ReadAsync() Took {sw.ElapsedMilliseconds}ms and totalRead: {totalRead}");
}
和结果:
Read() Took 162ms and totalRead: 162835040
ReadAsync() Took 15597ms and totalRead: 162835040
ReadAsync()大约慢100倍。我错过什么了吗?我能想到的唯一一件事是使用ReadAsync()创建和销毁任务的开销,但是开销有那么多吗?
更新:
我已经改变了上面的代码来反映@Cory的建议。有轻微的改进:
Read() Took 142ms and totalRead: 162835040
ReadAsync() Took 12288ms and totalRead: 162835040
当我按照@Alexandru的建议将读块大小增加到1MB时,结果更容易接受:
Read() Took 32ms and totalRead: 162835040
ReadAsync() Took 76ms and totalRead: 162835040
因此,它向我暗示确实是任务数量的开销导致了速度变慢。但是,如果任务的创建和销毁只需要100µs,那么对于小块大小来说,事情仍然不能真正增加速度。
如果你正在做异步,坚持使用大缓冲区,并确保在FileStream构造函数中打开异步模式,你应该没问题。像这样等待的异步方法将会在当前线程中捕获和退出(注意,当前线程是UI线程,它可能会被任何其他异步方法所延迟,从而促进相同的线程捕获和退出),因此如果你有大量的调用(想象调用一个新的线程构造函数并等待它完成大约100K次),那么在这个过程中会有一些开销。特别是如果你正在处理一个UI应用程序,你需要等待UI线程空闲,以便在异步函数完成后重新进入它)。因此,为了减少这些调用,我们只需读取更大的数据增量,并通过增加缓冲区大小将应用程序的重点放在一次读取更多数据上。请确保在发布模式下测试此代码,以便我们可以使用所有编译器优化,并且调试器不会减慢我们的速度:
class Program
{
static void Main(string[] args)
{
DoStuff();
Console.ReadLine();
}
public static async void DoStuff()
{
var filename = @"C:'Example.txt";
var sw = new Stopwatch();
sw.Start();
ReadAllFile(filename);
sw.Stop();
Console.WriteLine("Sync: " + sw.Elapsed);
sw.Restart();
await ReadAllFileAsync(filename);
sw.Stop();
Console.WriteLine("Async: " + sw.Elapsed);
}
static void ReadAllFile(string filename)
{
byte[] buffer = new byte[131072];
using (var file = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read, buffer.Length, false))
while (true)
if (file.Read(buffer, 0, buffer.Length) <= 0)
break;
}
static async Task ReadAllFileAsync(string filename)
{
byte[] buffer = new byte[131072];
using (var file = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read, buffer.Length, true))
while (true)
if ((await file.ReadAsync(buffer, 0, buffer.Length)) <= 0)
break;
}
}
结果:
同步:00:00:00.3092809
异步:00:00:00.5541262
非常微不足道的……文件大小约1gb
让我们说我更大,一个1mb的缓冲区,也就是new byte[1048576]
(拜托,现在每个人都有1mb的RAM):
同步:00:00:00.2925763
异步:00:00:00.3402034
那么只有百分之几秒的差别。如果你眨眼,你会错过的。
您的方法签名表明您正在从WPF应用程序中执行此操作。虽然阻塞代码将在此期间占用UI线程,但每次异步操作完成时,异步代码将被迫通过UI消息队列,减慢其速度并与任何UI消息竞争。你应该试着从UI线程中删除它,像这样:
void Button_Click(object sender, RoutedEventArgs e)
{
Task.Run(() => Button_Click_Impl());
}
async Task Button_Click_Impl()
{
// put code here.
}
接下来,以异步模式打开文件。如果你不这样做,异步是模拟的,并且会慢得多:
new FileStream(theFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096,
FileOptions.Asynchronous | FileOptions.SequentialScan)
最后,您还可以使用ConfigureAwait(false)
提取一些小性能,以避免在线程之间移动:
readSize = await fs.ReadAsync(data, 0, 1024).ConfigureAwait(false);
单个ReadAsync
操作的开销比单个Read
操作要高得多(特别是在打开文件时没有使用正确的模式时,请参阅其他答案)。如果最终整个文件都在内存中,只需查询文件的大小,分配一个足够大的缓冲区并一次读取所有文件。否则,您仍然可以将缓冲区大小增加到例如32 MiB,或者如果您期望更大的文件大小甚至更大。这将大大加快一切。
只有在每个块都有相当多的cpu工作时才会启动新任务。否则,UI应该由ReadAsync
操作(使用足够大的缓冲区)占用它们的时间来保持响应(如果它立即完成,您可能仍然阻塞UI,参见Task.Yield()
的注释)。