JavaRush /Курси /C# SELF /Асинхронна робота з текстовими файлами

Асинхронна робота з текстовими файлами

C# SELF
Рівень 42 , Лекція 2
Відкрита

1. Вступ

Згадайте наш приклад із завантаженням великого зображення. Поки воно завантажується, застосунок «зависає». Те саме відбувається і з текстовими файлами, особливо, якщо вони великі (лог‑файли на десятки гігабайт, великі CSV‑звіти, резервні копії баз даних у текстовому форматі).

Уявіть, що ви пишете застосунок, який:

Аналізує величезний лог‑файл, щоб знайти помилки. Якщо читатимете його синхронно, ваш користувацький інтерфейс просто «зависне» на кілька секунд або навіть хвилин, поки операція не завершиться. Користувач подумає, що програма зламалася.

Записує дані у звітний файл у міру їх генерації. Якщо запис блокує основний потік, то й генерація даних, і робота з інтерфейсом страждатимуть.

Веб‑сервер, який має обслуговувати тисячі запитів. Кожен запит може вимагати читання чи запису файлу. Якщо кожна така операція введення‑виведення буде синхронною, то потоки сервера простоюватимуть, очікуючи на диск, і сервер швидко «захлинеться» від напливу запитів.

У таких сценаріях асинхронне введення‑виведення — не просто «зручна можливість», а життєва необхідність. Асинхронність дає змогу вашому застосунку не простоювати, поки диск «думає», а займатися чимось корисним (наприклад, оновлювати інтерфейс, обробляти інші запити або виконувати обчислення).

Базові поняття: 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. Практичне застосування: де це використовується?

  • Веб‑розробка (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-блоки або using-оголошення (C# 8+) для гарантованого закриття й скидання буферів.

Розмір буфера (bufferSize): StreamReader/StreamWriter уже оптимізовані, але за особливих вимог можна поекспериментувати. За замовчуванням це комфортні значення (у FileStream часто вказують 4096 байт).

Обробка помилок: асинхронні методи так само викидають винятки. Оберніть операції в try-catch. Виняток «спливе» під час виконання await над відповідним Task.

ConfigureAwait: у бібліотеках і веб‑сценаріях, де не потрібен контекст синхронізації (GUI), використовуйте await SomeAsync().ConfigureAwait(false). Це зменшує накладні витрати на перемикання контексту. У консольних і UI‑застосунках зазвичай можна не використовувати.

Практикуйтеся — і скоро async Task стане таким самим звичним, як Console.WriteLine.

9. Типові помилки та важливі нюанси асинхронних файлових операцій

Якщо не використовувати await (а просто викликати метод із суфіксом Async), ви отримаєте обʼєкт Task, але результат не буде автоматично очікуватися. Його потрібно дочекатися через await або дочекатися явно (зазвичай це небажано).

Неможливо коректно очікувати асинхронні методи із синхронного коду без «підняття» async угору по стеку викликів. Використання .Result або .GetAwaiter().GetResult() може призводити до взаємних блокувань — краще перетворити викликаючі методи на async.

Не читайте й не пишіть один і той самий файл одночасно (навіть асинхронно). Це загрожує умовами гонки та пошкодженням даних.

Асинхронність вивільняє потік, що викликає операцію, але не робить операції швидшими: якщо диск або мережа повільні, асинхронно буде так само повільно — просто без блокування UI чи робочих потоків.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ