Рекомендации по записи в файлы
Важные API
Иногда разработчики сталкиваются с рядом распространенных проблем, когда используют методы Write классов FileIO и PathIO для выполнения операций ввода-вывода с файловой системой. Например, распространены следующие проблемы.
- Файл записан частично.
- Приложение получает исключение при вызове одного из методов.
- После выполнения операций остаются TMP-файлы с именем, аналогичным имени конечного файла.
К методам Write классов FileIO и PathIO относятся следующие:
- WriteBufferAsync
- WriteBytesAsync
- WriteLinesAsync
- WriteTextAsync
В этой статье описывается, как работают эти методы, чтобы разработчикам было понятнее, когда и как их использовать. В статье представлены рекомендации. Она не является решением для всех возможных проблем с файловым вводом-выводом.
Примечание.
В этой статье рассматриваются методы FileIO в примерах и обсуждениях. Тем не менее, методы PathIO следуют аналогичной схеме, и большинство рекомендаций и инструкций в этой статье также применимы к ним.
Удобство и управление
Объект StorageFile не является дескриптором файла, как в собственной модели программирования Win32. Вместо этого StorageFile является представлением файла с методами для управления его содержимым.
Понимание этой концепции пригодится при выполнении операций ввода-вывода с StorageFile. Например, в разделе Запись в файл представлены три способа записи в файл:
- с помощью метода FileIO.WriteTextAsync;
- путем создания буфера и последующего вызова метода FileIO.WriteBufferAsync;
- Четырехэтапная модель с помощью потока:
- открытие файла для получения потока;
- получение потока вывода;
- создание объекта DataWriter и вызов соответствующего метода Write;
- фиксация данных в модуле записи данных и запись на диск выходного потока.
Первые два сценария чаще всего используются в приложениях. Запись в файл одной операцией проще программировать и поддерживать. Кроме того, это снимает с приложения ответственность за множество сложностей файлового ввода-вывода. Тем не менее это удобство имеет свою цену: потеря контроля над всей операцией и возможности обнаруживать ошибки в определенных точках.
Транзакционная модель
Методы Write классов FileIO и PathIO охватывают шаги третьей модели записи, описанной выше, с добавлением уровня. Этот уровень инкапсулирован в транзакции хранилища.
Чтобы защитить целостность исходного файла в случае сбоя при записи данных, методы Write используют транзакционную модель, открывая файл с помощью метода OpenTransactedWriteAsync. В этом процессе создается объект StorageStreamTransaction. После создания этого объекта транзакции интерфейсы API записывают данные, как это реализовано в примере доступа к файлам или в примере кода в статье StorageStreamTransaction.
На следующей схеме показаны базовые задачи, выполняемые методом WriteTextAsync в успешной операции записи. На этом рисунке показано упрощенное представление операции. Например, на нем пропущены такие действия, как кодирование и асинхронное завершение разных потоков.
Преимущества использования методов Write классов FileIO и PathIO вместо более сложной модели из четырех шагов с использованием потока следующие:
- Один вызов API для обработки всех промежуточных шагов, включая ошибки.
- В случае сбоя исходный файл сохраняется.
- Состояние системы сохраняется как можно более чистым.
Однако ввиду множества возможных промежуточных точек сбоя существует повышенная вероятность ошибки. При возникновении ошибки может быть сложно понять, где произошел сбой процесса. В следующих разделах рассматриваются некоторые сбои, которые могут возникнуть при использовании методов Write, и предлагаются возможные решения.
Распространенные коды ошибок при использовании методов Write классов FileIO и PathIO
В этой таблице представлены распространенные коды ошибок, с которыми разработчики приложений могут столкнуться при использовании методов Write. Шаги, описанные в таблице, соответствуют шагам на предыдущей схеме.
Имя ошибки (значение) | Шаги | Причины | Решения |
---|---|---|---|
ERROR_ACCESS_DENIED (0X80070005) | 5 | Возможно, исходный файл помечен для удаления предыдущей операцией. | Повторите операцию. Обеспечьте синхронизацию доступа к файлу. |
ERROR_SHARING_VIOLATION (0x80070020) | 5 | Исходный файл открыт другим сеансом монопольной записи. | Повторите операцию. Обеспечьте синхронизацию доступа к файлу. |
ERROR_UNABLE_TO_REMOVE_REPLACED (0x80070497) | 19–20 | Исходный файл (file.txt) не может быть заменен, так как он используется. Другой процесс или операция получила доступ к файлу, прежде чем его удалось заменить. | Повторите операцию. Обеспечьте синхронизацию доступа к файлу. |
ERROR_DISK_FULL (0x80070070) | 7, 14, 16, 20 | Транзакционная модель создает дополнительный файл, и это требует дополнительного места на диске. | |
ERROR_OUTOFMEMORY (0x8007000E) | 14, 16 | Это может произойти из-за нескольких незавершенных операций ввода-вывода или большого размера файлов. | Более детальный подход с контролем потока может устранить эту ошибку. |
E_FAIL (0x80004005) | Любое | Разное | Повторите операцию. Если проблема не исчезла, возможно, это ошибка платформы, и приложение должно быть завершено, так как оно находится в несогласованном состоянии. |
Дополнительные рекомендации по состояниям файлов, которые могут привести к ошибкам
Помимо ошибок, возвращаемых методами Write, ознакомьтесь с некоторыми рекомендациями по реагированию приложения на запись в файл.
Данные записываются в файл только в том случае, если операция завершена
Приложение не должно делать какие-либо предположения о данных в файле, пока выполняется операция записи. Попытка обращения к файлу до завершения операции может привести к несогласованности данных. Ваше приложение должно отвечать за отслеживание незавершенных операций ввода-вывода.
Читатели
Если файл, в который выполняется запись, также используется "мягким" средством чтения (то есть открыт с помощью FileAccessMode.Read), то последующие операции чтения будут завершаться ошибкой ERROR_OPLOCK_HANDLE_CLOSED (0x80070323). Иногда приложения пытаются повторно открыть файл для чтения, пока выполняется операция Write. Это может привести к состязанию за доступ, и операция Write в итоге завершится сбоем, пытаясь перезаписать исходный файл, так как он не сможет быть заменен.
Файлы из KnownFolders
Ваше приложение может быть не единственным приложением, которое пытается получить доступ к файлу, расположенному в любой из папок KnownFolders. Нет никакой гарантии, что после успешного выполнения операции содержимое, которое приложение записало в файл, останется без изменений при следующей попытке считать этот файл. Кроме того, в этом сценарии распространены ошибки совместного использования и отказа в доступе.
Конфликты при операциях ввода-вывода
Вероятность ошибок параллельной обработки можно снизить, если ваше приложение использует методы Write для файлов в своих локальных данных, но по-прежнему следует проявлять осторожность. Если одновременно отправляется несколько операций Write с файлом, нет никакой гарантии того, какие данные в итоге окажутся в этом файле. Чтобы избежать этого, мы рекомендуем, чтобы ваше приложение сериализовало операции Write с файлом.
~TMP-файлы
Иногда, если операция принудительно отменяется (например, когда приложение приостановлено или завершено операционной системой), транзакция не фиксируется или не закрывается соответствующим образом. После этого могут остаться файлы с расширением ~TMP. Рассмотрите возможность удаления этих временных файлов (если они существуют в локальных данных приложения) при обработке активации приложения.
Рекомендации по типам файлов
Некоторые ошибки могут преобладать в зависимости от типа файлов, частоты обращения к ним и их размера. Как правило, приложению доступны три категории файлов:
- Файлы, созданные и измененные пользователем в папке локальных данных вашего приложения. Они создаются и изменяются только при использовании вашего приложения и существуют только в нем.
- Метаданные приложения. Ваше приложение использует эти файлы для отслеживания собственного состояния.
- Другие файлы в расположениях файловой системы, для которых ваше приложение объявило возможность доступа. Чаще всего они находятся в одной из папок KnownFolders.
Ваше приложение имеет полный контроль над первыми двумя категориями файлов, так как они являются частью файлов пакета приложения и используются исключительно вашим приложением. Что касается файлов из последней категории, ваше приложение должно читывать, что к ним могут одновременно обращаться другие приложения и службы ОС.
В зависимости от приложения, частота обращения к файлам может быть разной:
- Очень низкая. Обычно это файлы, открываемые сразу после запуска приложения и сохраняемые при приостановке работы приложения.
- Низкая. Это файлы, с которыми пользователь специально выполняет действия (такие как сохранение или загрузка).
- Средняя или высокая. Это файлы, в которых приложению требуется постоянно обновлять данные (например, для автосохранения или постоянного отслеживания метаданных).
Данные по размеру файлов и производительности можно изучить на следующей диаграмме для метода WriteBytesAsync. На этой диаграмме сравниваются длительность операции и размер файла. На ней отображается средняя производительность 10 000 операций для размера файла в управляемой среде.
На этой диаграмме намеренно пропущены значения времени на оси y, так как разное оборудование и конфигурации дадут разные абсолютные значения времени. Тем не менее мы наблюдали следующие устойчивые тенденции в тестах.
- Для очень маленьких файлов (<= 1 МБ): время выполнения операций неизменно быстрое.
- Для больших файлов (>1 МБ): время выполнения операций увеличивается экспоненциально.
Операции ввода-вывода во время приостановки приложения
Приложение должно обрабатывать приостановку, если вы хотите сохранить сведения о состоянии или метаданные для использования в последующих сеансах. Общие сведения о приостановке приложений доступны в разделе Жизненный цикл приложения и в этой записи блога.
Если только операционная система не предоставляет приложению режим расширенного выполнения, при приостановке у приложения есть 5 секунд на то, чтобы освободить все свои ресурсы и сохранить данные. Чтобы обеспечить наилучшую надежность и взаимодействие, всегда учитывайте, что время, выделяемое на обработку задач приостановки, ограничено. Используйте приведенные ниже рекомендации во время 5-секундного интервала времени, выделяемого для обработки задач приостановки.
- Пытайтесь свести к минимуму операции ввода-вывода, чтобы избежать состязания за доступ из-за операций записи на диск и освобождения.
- Избегайте операций записи в файлы, требующих сотен миллисекунд или больше для выполнения.
- Если приложение использует методы Write, учитывайте все промежуточные шаги, обязательные для этих методов.
Если приложение работает с небольшим объемом данных о состоянии во время приостановки, в большинстве случаев можно использовать методы Write для записи данных на диск. Тем не менее, если приложение использует большой объем данных о состоянии, рассмотрите возможность использования потоков для непосредственного сохранения данных. Это может снизить задержки, связанные с транзакционной моделью методов Write.
Вы можете ознакомиться с примером BasicSuspension.
Другие примеры и ресурсы
Ниже приведено несколько примеров и другие ресурсы для конкретных сценариев.
Пример кода для повтора файлового ввода-вывода
Ниже приведен пример псевдокода для повтора записи (C#) при условии, что запись требуется выполнить после того, как пользователь выберет файл для сохранения.
Windows.Storage.Pickers.FileSavePicker savePicker = new Windows.Storage.Pickers.FileSavePicker();
savePicker.FileTypeChoices.Add("Plain Text", new List<string>() { ".txt" });
Windows.Storage.StorageFile file = await savePicker.PickSaveFileAsync();
Int32 retryAttempts = 5;
const Int32 ERROR_ACCESS_DENIED = unchecked((Int32)0x80070005);
const Int32 ERROR_SHARING_VIOLATION = unchecked((Int32)0x80070020);
if (file != null)
{
// Application now has read/write access to the picked file.
while (retryAttempts > 0)
{
try
{
retryAttempts--;
await Windows.Storage.FileIO.WriteTextAsync(file, "Text to write to file");
break;
}
catch (Exception ex) when ((ex.HResult == ERROR_ACCESS_DENIED) ||
(ex.HResult == ERROR_SHARING_VIOLATION))
{
// This might be recovered by retrying, otherwise let the exception be raised.
// The app can decide to wait before retrying.
}
}
}
else
{
// The operation was cancelled in the picker dialog.
}
Синхронизация доступа к файлу
Блог по параллельному программированию для .NET является превосходным источником рекомендаций по параллельному программированию. В частности, в записи блога о AsyncReaderWriterLock описывается, как обеспечить монопольный доступ к файлу для записи, предоставляя параллельный доступ для чтения. Следует помнить, что сериализация операций ввода-вывода будет влиять на производительность.