文件流.与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,那么对于小块大小来说,事情仍然不能真正增加速度。

文件流.与Read()相比,ReadAsync非常慢

如果你正在做异步,坚持使用大缓冲区,并确保在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()的注释)。