Рекомендации по записи в файлы

Важные API

Иногда разработчики сталкиваются с рядом распространенных проблем, когда используют методы Write классов FileIO и PathIO для выполнения операций ввода-вывода с файловой системой. Например, распространены следующие проблемы.

  • Файл записан частично.
  • Приложение получает исключение при вызове одного из методов.
  • После выполнения операций остаются TMP-файлы с именем, аналогичным имени конечного файла.

К методам Write классов FileIO и PathIO относятся следующие:

  • WriteBufferAsync
  • WriteBytesAsync
  • WriteLinesAsync
  • WriteTextAsync

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

Примечание.

 В этой статье рассматриваются методы FileIO в примерах и обсуждениях. Тем не менее, методы PathIO следуют аналогичной схеме, и большинство рекомендаций и инструкций в этой статье также применимы к ним.

Удобство и управление

Объект StorageFile не является дескриптором файла, как в собственной модели программирования Win32. Вместо этого StorageFile является представлением файла с методами для управления его содержимым.

Понимание этой концепции пригодится при выполнении операций ввода-вывода с StorageFile. Например, в разделе Запись в файл представлены три способа записи в файл:

Первые два сценария чаще всего используются в приложениях. Запись в файл одной операцией проще программировать и поддерживать. Кроме того, это снимает с приложения ответственность за множество сложностей файлового ввода-вывода. Тем не менее это удобство имеет свою цену: потеря контроля над всей операцией и возможности обнаруживать ошибки в определенных точках.

Транзакционная модель

Методы Write классов FileIO и PathIO охватывают шаги третьей модели записи, описанной выше, с добавлением уровня. Этот уровень инкапсулирован в транзакции хранилища.

Чтобы защитить целостность исходного файла в случае сбоя при записи данных, методы Write используют транзакционную модель, открывая файл с помощью метода OpenTransactedWriteAsync. В этом процессе создается объект StorageStreamTransaction. После создания этого объекта транзакции интерфейсы API записывают данные, как это реализовано в примере доступа к файлам или в примере кода в статье StorageStreamTransaction.

На следующей схеме показаны базовые задачи, выполняемые методом WriteTextAsync в успешной операции записи. На этом рисунке показано упрощенное представление операции. Например, на нем пропущены такие действия, как кодирование и асинхронное завершение разных потоков.

UWP API call sequence diagram for writing to a file

Преимущества использования методов 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 операций для размера файла в управляемой среде.

WriteBytesAsync performance

На этой диаграмме намеренно пропущены значения времени на оси 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 описывается, как обеспечить монопольный доступ к файлу для записи, предоставляя параллельный доступ для чтения. Следует помнить, что сериализация операций ввода-вывода будет влиять на производительность.

См. также