CodeGym /课程 /C# SELF /文本文件的异步操作

文本文件的异步操作

C# SELF
第 42 级, 课程 2
可用

1. 介绍

回想我们加载大图片的例子。图片加载时,应用会“卡住”。处理文本文件时也一样,尤其是文件很大(几十 GB 的日志、巨量 CSV 报表、以文本形式的数据库备份)。

想象你在写一个应用,它会:

解析超大日志文件,以便找到错误。如果你同步读取,界面可能会卡住几秒甚至几分钟,用户会以为程序崩了。

边生成边写入报告文件。如果写入阻塞主线程,数据生成和界面交互都会受影响。

Web 服务器,需要处理成千上万的请求。每个请求可能要读写文件。如果这些 I/O 都是同步的,服务器线程会空等磁盘响应,服务器会很快被请求压垮。

在这些场景下,异步 I/O 不只是“nice-to-have”,而是必需的。它能让你的应用在磁盘“思考”时去做别的事(比如更新界面、处理其他请求或进行计算)。

基础概念: async/await 和 任务

  • 关键字 async 表示该方法可能包含“等待点”(await)。
  • 操作符 await 暂时交出控制权,直到异步任务完成(比如文件读取)。
  • 异步方法在做输入/输出时不会阻塞当前线程:在没有数据的时候线程是空闲的。

这就是处理文件时异步的基本魔法。

2. 用于文件的异步方法

在现代 .NET 版本中,几乎所有主要的文件操作类都有异步版本。对文本文件常用的有:

  • StreamReader.ReadLineAsync()
  • StreamReader.ReadToEndAsync()
  • StreamWriter.WriteLineAsync()
  • StreamWriter.WriteAsync()
  • 还有静态方法:File.ReadAllTextAsync()、File.WriteAllTextAsync() 等。
读取 写入
ReadLineAsync()
WriteLineAsync()
ReadToEndAsync()
WriteAsync()
File.ReadAllXAsync()
File.WriteAllXAsync()

3. 异步读取整个文本文件

我们来把整个文件读成一个字符串。通常用于小文件:配置、小日志。

using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string path = "input.txt";
        
        // 异步读取整个文件
        string fileContents = await File.ReadAllTextAsync(path);
        
        Console.WriteLine("文件内容:");
        Console.WriteLine(fileContents);
    }
}

注意:方法 Main 现在标注为 async Task Main()。从 C# 7.1 开始可以这么做。一个 await — 就能让一切异步运行!

4. 异步逐行读取大文件

当文件真的很大,把它一次性读进内存不是好主意。更好的办法是逐行读取:

using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string path = "biglog.txt";

        // 打开 StreamReader 用于异步读取
        using StreamReader reader = new StreamReader(path);

        string? line;
        while ((line = await reader.ReadLineAsync()) != null)
        {
            // 在这里可以处理该行(例如查找错误)
            Console.WriteLine(line);
        }
    }
}

这是怎么工作的?

每次调用 await reader.ReadLineAsync() 都会释放线程——这在文件在网络盘或云上时尤其有用。在几十万行并且还要同时和用户交互(比如在服务器 API 中)时,异步处理就非常关键。

5. 异步写入行到文件

同样地,可以异步写入文件(例如在生成报表时):

using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string path = "output.txt";

        using StreamWriter writer = new StreamWriter(path);

        for (int i = 0; i < 5; i++)
        {
            await writer.WriteLineAsync($"第{i + 1}行");
        }
        
        // 可以显式调用 FlushAsync 来确保写入
        await writer.FlushAsync();

        Console.WriteLine("数据已异步写入!");
    }
}

调用 FlushAsync() 并不总是必须的——关闭 StreamWriter 时缓冲会被冲洗。但如果你想立即确保“现在就写入”,就用它。

6. 多个异步文件操作的交互

假设你需要读取一个文本文件,同时把转换后的内容写到另一个文件:

using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string sourcePath = "even_biggerlog.txt";
        string destinationPath = "copy_biggerlog.txt";

        using StreamReader reader = new StreamReader(sourcePath);
        using StreamWriter writer = new StreamWriter(destinationPath);

        string? line;
        int linesProcessed = 0;

        while ((line = await reader.ReadLineAsync()) != null)
        {
            // 小魔法:把所有字母转换成大写
            string processed = line.ToUpperInvariant();

            await writer.WriteLineAsync(processed);
            linesProcessed++;
        }

        Console.WriteLine($"已处理的行数:{linesProcessed}");
    }
}

这里读和写都是异步的。每次 await 都会释放控制权,让应用去做别的事情。

7. 实际应用:哪里会用到?

  • Web 开发 (ASP.NET Core): 上传/下载文件不阻塞其他请求处理;服务器保持响应。
  • 桌面应用 (WPF, WinForms): 打开日志或保存报表时 UI 不会“卡死”。
  • 游戏引擎: 异步加载资源(贴图、模型)可以不打断动画和游戏逻辑。
  • 大数据处理: 逐行解析巨大的 CSV/JSON/XML,边读取边处理,减少内存占用。
  • 后台服务和守护进程: 日志记录、缓存、队列处理,能更高效地使用线程和磁盘。

总结:异步能帮你做出现代、响应快且可扩展的应用。“阻塞”很糟糕,异步更好!

8. 注意事项和最佳实践

别忘了 await! 如果你调用带有 Async 后缀的方法却不等它,得到的是一个 Task,但代码会继续往下跑,可能导致执行顺序错误。

// 不好:忘了 await
FileManager.ReadTextFileAsync("nonexistent.txt"); // 会启动,但 Main 会继续往下走
Console.WriteLine("我马上就执行了,尽管文件还在读(或者已经抛错)!这很糟糕!");

编译器通常会提醒你忘了 await,但不会阻止编译通过。

using 用于所有实现了 IDisposable 的东西:所有流(FileStream、StreamReader、StreamWriter)都要正确释放。使用 using 块或 C# 8+ 的 using 声明来保证关闭和冲刷缓冲。

缓冲区大小(bufferSize):StreamReader/StreamWriter 已经做了优化,但在特殊需求下可以调试试验。默认值通常是合适的(在 FileStream 中常见的默认是 4096 字节)。

错误处理:异步方法也会抛异常。把操作包在 try-catch 里。异常会在对相应 Task 执行 await 时“浮出水面”。

ConfigureAwait:在库和 Web 场景(不需要同步上下文,比如 GUI)中,使用 await SomeAsync().ConfigureAwait(false) 可以减少切换上下文的开销。在控制台和 UI 应用里通常可以省略它。

多练习——很快 async Task 就会像 Console.WriteLine 一样顺手了。

9. 常见错误和重要注意点

如果不使用 await(只是调用带有 Async 的方法),你会得到一个 Task,但结果不会自动等待。你需要通过 await 或者显式等待(通常不推荐)来等待它完成。

不能在同步代码中等待异步方法,除非把 async 向上“抛”到调用栈里。使用 .Result 或 .GetAwaiter().GetResult() 可能导致死锁——最好把调用方也改成 async。

不要同时读取和写入同一个文件(即便是异步的)。那样容易产生竞态条件和数据损坏。

异步会释放调用线程,但不会让底层操作更快:如果磁盘或网络慢,异步仍会很慢——只是不会阻塞 UI 或工作线程。

2
任务
C# SELF, 第 42 级, 课程 2
已锁定
异步按行读取文件
异步按行读取文件
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION