Как стать автором
Обновить

Как работает буферизация в Rust: стандартная библиотека std::io::{BufReader, BufWriter}

Уровень сложностиПростой

Разбор вопроса с точки зрения новичка в программировании, так что некоторые выводы могут быть не верны, названия категорий — ошибочны.

Для себя задался вопросом, как работает буферизация для Rust и зачем она нужна при чтении файла с текстом. Оказалось, что ответ найти сложно: про буферизацию чаще всего разбирают либо в контексте C++, либо Java, но первый вариант сложноват, а второй не подходит из-за разных подходов к работе с памятью.

Казалось бы, можно открыть стандартную библиотеку Rust, найти документацию std::io::{BufReader, BufWriter} и закончить на этом. Однако моя цель разобрать вопрос на более фундаментальном уровне, поэтому ниже попробую сделать это, несмотря на пробелы в базовых знаниях.

С чего начать

Начать нужно вот с этого видео, чтобы приобрести базовое понимание стека (stack) и кучи (heap). Если вы этого не понимаете, дальше читать бессмысленно. Достаточно досмотреть до 5:48, дальше там не такая важная информация для нашего вопроса.

Разбираем, где нужна рассматриваемая буферизация и в чём её смысл

  1. При изучении основ Rust вы непременно столкнётесь с задачами, где нужно открыть и прочитать/редактировать текстовый файл. Это первый случай использования буферизации. Здесь её используют для увеличения скорости работы приложения.

  2. Стандартная библиотека рассказывает о втором случае. Это обращение к памяти на сервере (что-то связанное с TcpStream). Здесь буферизация нужна как для скорости, так и для безопасности. Так, без неё приложению приходится чаще обращаться к памяти сервера с помощью системных вызовов system calls (syscalls), между которыми появляются неприятные паузы, и в целом частые syscalls увеличивают вероятность сбоя (например, наверное, при разрыве соединения с сервером).

Чтение файла без буферизации

Итак, у нас есть файл с текстом. Пусть он называется "hello.txt", и для работы с ним нам нужно его открыть:

use std::{fs::File, io::{Write, Read}};
use std::fs;

// snip

let f = File::open("hello.txt").unwrap();

Первые две строчки кода с use подключают модули стандартной библиотеки для работы с файлами. Благодаря этому мы можем открыть файл с помощью File::open("../hello.txt"), где в скобках путь к файлу.

Далее мы задаём переменную f. Она фактически открывает канал между жёстким диском и оперативной памятью (где стек, куча и прочее в моём понимании). То есть переменная f — это и есть открытый канал. Он закроется, когда f выйдет из зоны видимости за скобки {} или если мы дропнем переменную через drop(f).

На этом этапе ни в стеке, ни в куче данных из файла нет. Чтобы это исправить, можно переписать текст в новую переменную строкового типа без использования буферизации. Это можно сделать так:

let mut text: String = String::new();
f.read_to_string(text);

С помощью переменной text мы зарезервировали память в стеке и куче, а с помощью метода read_to_string(переменная) переписали в text данные из файла. Примечание: последние три строки можно переписать в одну let text = fs::read_to_string("hello.txt").unwrap(), но для понимания процесса приходится писать длинный код.

На этом этапе все данные из файла дублируются в оперативной памяти (стек и куча). Мы можем прекрасно с ними работать. Но есть минус: если у нас файл "hello.txt" был 1 Мб, то теперь весь этот мегабайт засоряет нам оперативную память. В этом случае нужно рассмотреть буферизацию.

Как работает буферизация в Rust

Подключаем нужные модули стандартной библиотеки:

use std::io::{BufReader, BufRead};

Открываем канал и инициируем буферизацию для работы с нашим файлом:

let f = File::open("hello.txt").unwrap();
let text = BufReader::new(f);

С помощью BufReader::new(f) мы резервируем в стеке и куче место под чтение из файла. Только это будет далеко не 1 Мб. Так как, согласно стандартной библиотеке, BufReader использует методы для работы со строками, предполагаю, что в быстрой памяти он резервирует место всего под две-несколько строк.

Как только в нашей программе начинается обработка первых строк, в буфер из файла с жёсткого диска подгружаются следующие. Ключевая фраза здесь «обработка первых строк». Например, мы можем перебирать строки циклом или итератором, внося какие-либо изменения. Дальше мы можем собрать их, например, в строку с помощью метода collect(). Например, так:

let string: String = text.lines().map(|s| format!("V! {}", s.unwrap())).collect();

Здесь мы разбираем текст из "hello.txt" по строкам, затем в начало каждой строки добавляем «V! » и собираем это в единый текст в строковую переменную string.

Но что если нам не нужно обрабатывать каждую строку, а нужно просто копировать текст целиком из переменной text в переменную string? Для чего? Например, хотим таким извращённым способом записать текст в новый файл. В этом случае смысл буферизации теряется. Мы в любом случае будем резервировать 1 Мб в быстрой памяти, и буферизация будет лишь извращением. Думаю, это ключевой момент для понимания.

Буду благодарен за обратную связь. Будет время, доработаю.

Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.