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 лекция
Недоступна
Асинхронная запись большого файла
Асинхронная запись большого файла
Комментарии (7)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Ra Уровень 64 Student
26 июня 2026
На Java пишем утилитный класс с гринтредами и запускаем почти также немногословно

public class Main {
    public static void main(String[] args) {
        String fileName = "report.txt";

        // Вызов в стиле Java 21 - Async утилитный класс
        Async.run(
            () -> writeToFile(fileName),
            () -> System.out.println("Готово!")
        );
    }

    private static void writeToFile(String fileName) {
        try {
            System.out.println("Начинаем запись файла: " + fileName);
            Thread.sleep(10000); // Имитация долгого I/O (поток ОС НЕ блокируется)
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
Ra Уровень 64 Student
26 июня 2026
C#: Чтобы бизнес-логика не страдала от «вирусности» асинхронности, её превращают в конвейер (Pipeline) с помощью System.Threading.Channels. Задача — оставить async/await только на входе и выходе, сделав промежуточные этапы линейными. Почему в C# нельзя сделать «как в Java»?Архитектурное разделение в .NET: В C# асинхронность сделана на уровне языка и компилятора (конечные автоматы, генерируемые из async/await), а в Java виртуальные потоки сделаны на уровне виртуальной машины (JVM). CLR (.NET Runtime) не умеет незаметно для программиста приостанавливать поток ОС, если внутри него произошел блокирующий вызов. Экосистема: Чтобы виртуальные потоки в Java работали, разработчикам пришлось переписать все стандартные библиотеки ввода-вывода (сокеты, файлы) под капотом JVM. В .NET вся экосистема (Entity Framework, HttpClient, драйверы БД) изначально спроектирована под явное использование Task и async/await.
Ra Уровень 64 Student
26 июня 2026
Виртуальные потоки в Java не являются полным функциональным заменителем async/await из C#. Они решают одну проблему — масштабирование при вводе-выводе (I/O), но концептуально работают совершенно иначе. Сравнение их ключевых отличий: Проблема «цветных функций» (Virality): В C# асинхронный метод требует ключевых слов async/await и возвращает Task. Из-за этого «заражается» весь стек вызовов, и вы не можете легко вызвать асинхронный метод из синхронного. Виртуальные потоки в Java позволяют писать код в классическом синхронном стиле, не требуя никаких специальных пометок в коде для асинхронных операций — среда выполнения берет это на себя. Смысл многозадачности: async/await не создает новые потоки, это лишь синтаксический сахар для конечного автомата, который освобождает текущий поток во время ожидания. Зеленые треды (виртуальные потоки) — это полноценные потоки с точки зрения программиста, просто их исполнение JVM динамически мапит на аппаратные (ядерные) потоки ОС. Ограничения пулов потоков: Зеленые треды виртуализируют потоки ОС. async/await в C# работает в рамках пула потоков (обычно ThreadPool ОС), эффективно переиспользуя их. Виртуальные потоки в Java можно создавать миллионами без перегрузки ОС, что делает их идеальными для блокирующего I/O. Стек вызовов: У задач async/await в C# «плоский» стек вызовов, и при отладке вы теряете полный стек, если он не обрабатывается специальными механизмами. Виртуальные потоки сохраняют полноценный стек вызовов, что упрощает отладку (stack traces). Если async/await в C# кажется вам неудобным (в частности, из-за проблемы «заразности» функций и необходимости везде прописывать ключевые слова), в экосистеме .NET иногда применяют альтернативные подходы для более чистого асинхронного кода (например, библиотеки вроде Mediator или System.Threading.Channels).
Ra Уровень 64 Student
26 июня 2026
Имхо это лишний гемор, если есть green threads. Не вижу плюсов у async перед ними, а минусов много. Переубедите плиз.
Slevin Уровень 59
24 февраля 2026
"пользователь может открыть файл по сети, а там скорость — как у черепахи на рабочий обед." Где вы такие выражения берёте? Их у вас тоже ИИ придумывает?
Александр Уровень 62
26 марта 2026
да, похоже на глюк)
Ra Уровень 64 Student
26 июня 2026
Представил работающую черепаху и Пелевин вспомнился про белочек. И какая логика, черепаха спешит или нет на рабочий обед, по сравнению с обычным?