JavaRush /Курсы /C# SELF /Асинхронное чтение и запись файлов (

Асинхронное чтение и запись файлов ( ReadAsync/ WriteAsync)

C# SELF
42 уровень , 1 лекция
Открыта

1. Введение

Представьте, что вы — дирижёр оркестра (вашего приложения). Если каждый раз, когда скрипач настраивает свою скрипку (долгое I/O), вы ждёте его, прежде чем перейти к другим инструментам, весь оркестр будет стоять. Но если скрипач говорит: "Я настроюсь, а вы пока продолжайте, я подам знак, когда буду готов", — это асинхронность!

В мире C# и .NET 9 для такой "многозадачности без замирания" существуют специальные инструменты. Главные герои сегодняшнего дня – это асинхронные версии методов Read и Write, которые называются ReadAsync и WriteAsync.

Они позволяют вам инициировать операцию чтения или записи и тут же "отпустить" текущий поток выполнения, чтобы он мог заниматься чем-то другим. Когда операция ввода-вывода завершится (например, данные будут прочитаны с диска или записаны на него), ваш код "проснется" и продолжит работу с того места, где остановился.

Для использования этих методов нам понадобятся два волшебных слова, которые пришли в C# ещё в 2012 году с версией 5.0 (а сейчас, в C# 14, они уже как родные!):

  • async: Это модификатор, который вы добавляете к методу, чтобы сказать компилятору: "Внутри этого метода могут быть асинхронные операции, и я буду использовать await".
  • await: Это оператор, который вы используете перед вызовом асинхронной операции (вроде ReadAsync или WriteAsync). Он означает: "Начни эту операцию, но не жди её завершения здесь. Отдай управление вызывающему коду и вернись, когда операция будет готова".

Не переживайте, если эти понятия пока кажутся немного туманными. У нас будет целый отдельный уровень, посвящённый async и await (Уровень 58), где мы разберем их подноготную. Сейчас важно понять, что они помогают нам "не блокировать" основной поток.

2. ReadAsync: читаем не торопясь

Метод ReadAsync позволяет асинхронно читать данные из потока. Вместо того чтобы ждать, пока байты будут считаны с диска, вы начинаете чтение и сразу переключаетесь на другие задачи.

Вот как выглядит его основная сигнатура для чтения в буфер:

public virtual ValueTask<int> ReadAsync(
    byte[] buffer,
    int offset,
    int count,
    CancellationToken cancellationToken = default
)

Или, что чаще используется в современном C# (и .NET 9), с использованием Memory<byte>:

public virtual ValueTask<int> ReadAsync(
    Memory<byte> buffer,
    CancellationToken cancellationToken = default
)

Разберем параметры:

  • buffer: Это массив byte (или Memory<byte>), куда будут прочитаны данные. Помните, мы говорили про буферы для оптимизации? Здесь они используются точно так же, но уже для асинхронных операций.
  • offset: Смещение в buffer, куда начать запись прочитанных байтов.
  • count: Максимальное количество байтов для чтения.
  • CancellationToken cancellationToken: Это очень полезный параметр, позволяющий отменить операцию, если она больше не нужна (например, пользователь закрыл приложение или нажал кнопку "Отмена").
  • ValueTask<int>: Это "обещание" того, что когда операция завершится, она вернет целое число (int), которое будет равно количеству прочитанных байтов. ValueTask — оптимизированная версия Task для сценариев, когда результат может быть доступен синхронно или асинхронно.

Пример 1: Асинхронное чтение файла

Представим, что у нас есть большой текстовый файл, и мы хотим его читать не блокируя основной поток. Вот базовый пример:

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

class Program
{
    // Асинхронная функция для чтения файла и подсчёта количества строк
    public static async Task<int> CountLinesAsync(string filePath)
    {
        int lineCount = 0;
        // Асинхронное открытие файла
        using FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true);
        using StreamReader reader = new StreamReader(fileStream, Encoding.UTF8);

        string? line;
        while ((line = await reader.ReadLineAsync()) != null)
        {
            lineCount++;
        }
        return lineCount;
    }

    static async Task Main()
    {
        string filename = "bigtext.txt";
        int count = await CountLinesAsync(filename);
        Console.WriteLine($"В файле {filename} строк: {count}");
    }
}

Комментарии к коду:

  • Обратите внимание на параметр useAsync: true у FileStream. Это важно для настоящей асинхронности.
  • Мы используем await к ReadLineAsync, чтобы не блокировать поток, пока читается строчка.
  • Метод Main теперь асинхронный (C# 7+ позволяет это делать).

Если бы это было реальное графическое приложение, то во время чтения файла (пока ReadAsync ждёт данные с диска) пользователь мог бы нажимать кнопки, скроллить и выполнять другие действия, потому что основной поток UI не был бы заблокирован. В консольном приложении это менее заметно, но принцип тот же.

3. WriteAsync: пишем без промедления

Аналогично ReadAsync, метод WriteAsync позволяет асинхронно записывать данные в поток. Это очень полезно, когда вам нужно записать много данных, не заставляя приложение ждать завершения записи на диск.

Основные сигнатуры:

public virtual ValueTask WriteAsync(
    byte[] buffer,
    int offset,
    int count,
    CancellationToken cancellationToken = default
)

И с использованием ReadOnlyMemory<byte> (для записи мы не модифицируем буфер):

public virtual ValueTask WriteAsync(
    ReadOnlyMemory<byte> buffer,
    CancellationToken cancellationToken = default
)

Параметры похожи на ReadAsync:

  • buffer: Массив byte (или ReadOnlyMemory<byte>), содержащий данные для записи.
  • offset: Смещение в buffer, откуда начать чтение данных для записи.
  • count: Количество байтов для записи.
  • CancellationToken cancellationToken: Для отмены операции.
  • ValueTask: Возвращаемого значения нет, т.к. количество записываемых байтов задаётся параметром count.

Пример 2: Асинхронная запись файла

А теперь давайте что-нибудь запишем в файл — тоже асинхронно.

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

class Program
{
    public static async Task WriteTestAsync(string filePath)
    {
        using FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: true);
        using StreamWriter writer = new StreamWriter(fs, Encoding.UTF8);

        for (int i = 0; i < 10000; i++)
        {
            await writer.WriteLineAsync($"Строка номер {i}");
        }
    }

    static async Task Main()
    {
        string filename = "testout.txt";
        await WriteTestAsync(filename);
        Console.WriteLine($"Запись {filename} завершена.");
    }
}

Здесь цикл записывает 10000 строк, а основной поток не блокируется: если бы это был GUI-приложение, интерфейс бы не "вис".

Благодаря async и await, наше консольное приложение теперь может выполнять операцию копирования файла, при этом оставаясь "отзывчивым" к вводу пользователя (в нашем случае, к нажатию Enter для отмены). Это фундаментальный принцип написания современных, производительных и отзывчивых приложений на C#.

4. Полезные нюансы

Визуализация: как работает асинхронное чтение/запись


┌───────────────────┐     Start Async Read    ┌────────────────────────────────┐
│Ваш код (UI/лоигка)│ ─────────────────────→  │  OS/E/S: асинхронная операция  │ 
└─────┬─────────────┘                         └───────┬────────────────────────┘
      │(делает что-то ещё)                       │(читает файл, ждёт диск)
      │<────────────────────────────────────────→│
      └─ Waits for Task, gets result  ←──────────┘

Примерная схема: пока диск медленно копается, код может делать что-то ещё. Только когда данные нужны — выполнение дожидается Task.

Практическое применение: где это реально нужно?

  • Десктопные приложения: если ваше приложение читает или пишет что-то крупное (лог-файлы, базы данных, видео), асинхронность — must-have. Даже если ваш компьютер "огонь", пользователь может открыть файл по сети, а там скорость — как у черепахи на рабочий обед.
  • Бэкенд или веб-приложения: одновременно к серверу могут стучаться десятки (а то и тысячи) пользователей. Если каждый поток блокируется на чтении файлов — все, здравствуй, тормоза и 502 Bad Gateway.
  • Мобильные приложения: если открыть файл или записать данные занимает время — пользователь увидит лаг. Используем асинхронность!
  • Любая массовая обработка файлов: любое приложение, работающее с коллекциями файлов (архиваторы, парсеры, анализаторы) — выигрывает от асинхронного I/O.

Синхронное vs Асинхронное чтение/запись

Метод Блокирует поток? Прост в реализации? Лучшая производительность? Удобство для UI/серверов
Синхронный (Read/Write) Да Да Нет Нет
Асинхронный (ReadAsync) Нет Почти Да Да

5. Нюансы и лучшие практики

Буферизация по-прежнему важна: Даже с ReadAsync и WriteAsync, чтение или запись по одному байту за раз всё ещё крайне неэффективно. Асинхронность снимает блокировку, но не магически ускоряет чтение каждого отдельного байта. Хорошие стартовые размеры буфера — 4096-8192 байт, для крупных файлов имеет смысл попробовать 65536 или 131072.

"Асинхронность должна быть повсюду" (Async All The Way Down): Если вы начали использовать async/await в одном месте, обычно следует протянуть этот подход по всей цепочке вызовов: C выполняет асинхронную операцию — значит C это async Task, B тоже async Task, и Aasync Task. Иначе возможны блокировки и даже дедлоки в UI-приложениях.

Обработка исключений: В асинхронном коде используйте обычные try-catch. Часто встречаются OperationCanceledException и IOException — обрабатывайте их явно.

Освобождение ресурсов (await using): Для потоков и других IDisposable-объектов корректно освобождайте ресурсы. Если тип реализует IAsyncDisposable, то await using вызовет DisposeAsync(); если только IDisposable — будет вызван Dispose().

Что происходит под капотом (кратко): При await компилятор превращает метод в конечный автомат: операция запускается, метод "ставится на паузу", управление возвращается вызывающему коду. По готовности результата SynchronizationContext (в UI) или ThreadPool (в консоли/на сервере) возобновит выполнение с места остановки. Это позволяет одному потоку обслуживать множество "приостановленных" задач без блокировок.

В заключение, асинхронное программирование с async и await — это мощный инструмент для создания отзывчивых и масштабируемых приложений. Оно позволяет вашему коду эффективно использовать системные ресурсы, не блокируя пользовательский интерфейс или потоки на сервере. Возможно, поначалу это будет казаться немного непривычным, но поверьте, это стоит освоения! В следующих лекциях мы продолжим углубляться в мир асинхронности и параллелизма. До встречи!

2
Задача
C# SELF, 42 уровень, 1 лекция
Недоступна
Асинхронная запись большого файла
Асинхронная запись большого файла
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ