向文件进行写入的最佳做法Best practices for writing to files

重要的 APIImportant APIs

开发人员在使用 FileIOPathIO 类的 Write 方法执行文件系统 I/O 操作时,偶尔会遇到一系列常见问题。Developers sometimes run into a set of common problems when using the Write methods of the FileIO and PathIO classes to perform file system I/O operations. 例如,这些常见问题包括:For example, common problems include:

  • 不完整地写入某个文件。A file is partially written.
  • 应用在调用某个方法时收到异常。The app receives an exception when calling one of the methods.
  • 操作留下一些文件名类似于目标文件名的 .TMP 文件。The operations leave behind .TMP files with a file name similar to the target file name.

FileIOPathIO 类的 Write 方法包括:The Write methods of the FileIO and PathIO classes include the following:

  • WriteBufferAsyncWriteBufferAsync
  • WriteBytesAsyncWriteBytesAsync
  • WriteLinesAsyncWriteLinesAsync
  • WriteTextAsyncWriteTextAsync

本文将提供有关这些方法的工作原理的详细信息,使开发人员能够更好地了解何时以及如何使用它们。This article provides details about how these methods work so developers understand better when and how to use them. 本文只会提供指导,而不会试图针对所有可能的文件 I/O 问题提供解决方法。This article provides guidelines and does not attempt to provide a solution for all possible file I/O problems.

备注

 本文会重点介绍示例和讨论内容中的 FileIO 方法。 This article focuses on the FileIO methods in examples and discussions. 但是,PathIO 方法遵循类似的模式,本文中的大部分指导同样适用于这些方法。However, the PathIO methods follow a similar pattern and most of the guidance in this article applies to those methods too.

便利性与控制度Convenience vs. control

StorageFile 对象并非本机 Win32 编程模型那样的文件句柄。A StorageFile object is not a file handle like the native Win32 programming model. StorageFile 是文件的一种表示形式,该文件包含用于操作其内容的方法。Instead, a StorageFile is a representation of a file with methods to manipulate its contents.

使用 StorageFile 执行 I/O 时,了解这一概念很有好处。Understanding this concept is useful when performing I/O with a StorageFile. 例如,写入文件部分演示了写入文件的三种方式:For example, the Writing to a file section presents three ways to write to a file:

前两种方案是应用最常用的方案。The first two scenarios are the ones most commonly used by apps. 以单个操作写入文件更易于编程和维护,同样,使应用不必要应对文件 I/O 存在的多种复杂性。Writing to the file in a single operation is easier to code and maintain, and it also removes the responsibility of the app from dealing with many of the complexities of file I/O. 但是,获得这种便利性也需要付出一定的代价:损失了整个操作的控制度,并且无法捕获特定时间点发生的错误。However, this convenience comes at a cost: the loss of control over the entire operation and the ability to catch errors at specific points.

事务模型The transactional model

FileIOPathIO 类的 Write 方法通过一个附加层整合了上述第三个写入模型的步骤。The Write methods of the FileIO and PathIO classes wrap the steps on the third write model described above, with an added layer. 此层封装在存储事务中。This layer is encapsulated in a storage transaction.

如果在写入数据时出错,为了保护原始文件的完整性,Write 方法将通过 OpenTransactedWriteAsync 打开该文件,以使用事务模型。To protect the integrity of the original file in case something goes wrong while writing the data, the Write methods use a transactional model by opening the file using OpenTransactedWriteAsync. 此过程将创建 StorageStreamTransaction 对象。This process creates a StorageStreamTransaction object. 创建此事务对象后,API 会按照 StorageStreamTransaction 一文中的文件访问示例或代码示例所述的类似方式写入数据。After this transaction object is created, the APIs write the data following a similar fashion to the File Access sample or the code example in the StorageStreamTransaction article.

下图演示了成功的写入操作中 WriteTextAsync 方法执行的基础任务。The following diagram illustrates the underlying tasks performed by the the WriteTextAsync method in a successful write operation. 此图提供了操作的简化视图。This illustration provides a simplified view of the operation. 例如,它跳过了在不同的线程上执行文本编码和异步完成的步骤。For example, it skips steps such as text encoding and async completion on different threads.

用于写入文件的 UWP API 调用顺序图

使用 FileIOPathIO 类的 Write 方法,而不是使用利用流的更复杂四步模型的优势包括:The advantages of using the Write methods of the FileIO and PathIO classes instead of the more complex four-step model using a stream are:

  • 执行一次 API 调用即可处理所有中间步骤,包括错误处理。One API call to handle all the intermediate steps, including errors.
  • 出错时可以保留原始文件。The original file is kept if something goes wrong.
  • 尽量保留干净的系统状态。The system state will try to be kept as clean as possible.

但是,由于存在许多的潜在中间故障点,发生失败的可能性也会增大。However, with so many possible intermediate points of failure, there’s an increased chance of failure. 发生错误时,可能难以了解过程在哪个位置失败。When an error occurs it may be difficult to understand where the process failed. 以下部分介绍了在使用 Write 方法时可能会遇到的一些失败,并提供了可能的解决方法。The following sections present some of the failures you might encounter when using the Write methods and provide possible solutions.

FileIO 和 PathIO 类的 Write 方法的常见错误代码Common error codes for Write methods of the FileIO and PathIO classes

下表列出了应用开发人员在使用 Write 方法时可能会遇到的常见错误代码。This table presents common error codes that app developers encounter when using the Write methods. 表中的步骤对应于上图中的步骤。The steps in the table correspond to steps in the previous diagram.

错误名称(值)Error name (value) 步骤Steps 原因Causes 解决方案Solutions
ERROR_ACCESS_DENIED (0X80070005)ERROR_ACCESS_DENIED (0X80070005) 55 原始文件可能已标记为删除(可能是在前一操作中标记的)。The original file might be marked for deletion, possibly from a previous operation. 重试操作。Retry the operation.
确保对文件的访问权限已同步。Ensure access to the file is synchronized.
ERROR_SHARING_VIOLATION (0x80070020)ERROR_SHARING_VIOLATION (0x80070020) 55 原始文件已由另一个排他写入操作打开。The original file is opened by another exclusive write. 重试操作。Retry the operation.
确保对文件的访问权限已同步。Ensure access to the file is synchronized.
ERROR_UNABLE_TO_REMOVE_REPLACED (0x80070497)ERROR_UNABLE_TO_REMOVE_REPLACED (0x80070497) 19-2019-20 原始文件 (file.txt) 已被使用,因此无法将其替换。The original file (file.txt) could not be replaced because it is in use. 在替换之前,另一个进程或操作已获取了该文件的访问权限。Another process or operation gained access to the file before it could be replaced. 重试操作。Retry the operation.
确保对文件的访问权限已同步。Ensure access to the file is synchronized.
ERROR_DISK_FULL (0x80070070)ERROR_DISK_FULL (0x80070070) 7、14、16、207, 14, 16, 20 事务处理模型创建了额外的文件,这消耗了额外的存储。The transacted model creates an extra file, and this consumes extra storage.
ERROR_OUTOFMEMORY (0x8007000E)ERROR_OUTOFMEMORY (0x8007000E) 14、1614, 16 此错误的原因可能是存在多个未完成的 I/O 操作,或文件很大。This can happen due to multiple outstanding I/O operations or large file sizes. 以更精细的方法控制流可能会解决该错误。A more granular approach by controlling the stream might resolve the error.
E_FAIL (0x80004005)E_FAIL (0x80004005) AnyAny 杂项Miscellaneous 重试操作。Retry the operation. 如果仍然失败,则可能表示平台出错,应用应该终止,因为它处于不一致状态。If it still fails, it might be a platform error and the app should terminate because it's in an inconsistent state.

可能导致出错的其他文件状态考虑因素Other considerations for file states that might lead to errors

除了 Write 方法返回的错误以外,下面还提供了有关应用在写入文件时预期可能会遇到的问题的指导。Apart from errors returned by the Write methods, here are some guidelines on what an app can expect when writing to a file.

当且仅当操作完成时,才会将数据写入文件Data was written to the file if and only if operation completed

当操作正在进行时,应用不应该对文件中的数据做出任何假设。Your app should not make any assumption about data in the file while a write operation is in progress. 在操作完成之前尝试访问文件可能会导致不一致的数据。Trying to access the file before an operation completes might lead to inconsistent data. 应用应该负责跟踪未完成的 I/O。Your app should be responsible of tracking outstanding I/Os.

ReadersReaders

如果写入到的文件同时由某个正常的读取器(即,文件是使用 FileAccessMode.Read 打开的),则后续读取将会失败并出现错误 ERROR_OPLOCK_HANDLE_CLOSED (0x80070323)。If the file that being written to is also being used by a polite reader (that is, opened with FileAccessMode.Read, subsequent reads will fail with an error ERROR_OPLOCK_HANDLE_CLOSED (0x80070323). 有时,当 Write 操作正在进行时,应用会重试再次打开文件进行读取。Sometimes apps retry opening the file for read again while the Write operation is ongoing. 这可能会导致争用状态,使 Write 最终在尝试覆盖原始文件时失败,因为无法替换该文件。This might result in a race condition on which the Write ultimately fails when trying to overwrite the original file because it cannot be replaced.

KnownFolders 中的文件Files from KnownFolders

你的应用可能不是唯一一个尝试访问任何 KnownFolders 中的文件的应用。Your app might not be the only app that is trying to access a file that resides on any of the KnownFolders. 无法保证当操作成功时,应用写入到该文件的内容在下一次尝试读取该文件时保持不变。There’s no guarantee that if the operation is successful, the contents an app wrote to the file will remain constant the next time it tries to read the file. 此外,在这种情况下,拒绝共享或访问错误会更常见。Also, sharing or access denied errors become more common under this scenario.

有冲突的 I/OConflicting I/O

如果应用对其本地数据中的文件使用 Write 方法,则可以减少出现并发错误的可能性,但仍需注意某些问题。The chances of concurrency errors can be lowered if our app uses the Write methods for files in its local data, but some caution is still required. 如果同时将多个 Write 发送到文件,则无法保证该文件中的最终数据是什么。If multiple Write operations are being sent concurrently to the file, there’s no guarantee about what data ends up in the file. 为了缓解此问题,我们建议让应用将发送到文件的 Write 操作序列化。To mitigate this, we recommend that your app serializes Write operations to the file.

~TMP 文件~TMP files

有时,如果强制取消操作(例如,如果应用已由 OS 挂起或终止),则不会提交或适当关闭事务。Occasionally, if the operation is forcefully cancelled (for example, if the app was suspended or terminated by the OS), the transaction is not committed or closed appropriately. 这可能会留下带有 .~TMP 扩展名的文件。This can leave behind files with a (.~TMP) extension. 在处理应用激活时,请考虑删除这些临时文件(如果应用的本地数据中存在这些文件)。Consider deleting these temporary files (if they exist in the app's local data) when handling the app activation.

文件类型相关的注意事项Considerations based on file types

根据文件的类型、其访问频率和大小,某些错误可能会变得更加普遍。Some errors can become more prevalent depending on the type of files, the frequency on which they’re accessed, and their file size. 通常情况下,应用可以访问三种类别的文件:Generally, there are three categories of files your app can access:

  • 由用户在应用的本地数据文件夹中创建和编辑的文件。Files created and edited by the user in your app's local data folder. 只能在使用应用时创建和编辑这些文件,并且它们只会在该应用中存在。These are created and edited only while using your app, and they exist only within the app.
  • 应用元数据。App metadata. 应用使用这些文件来跟踪自身的状态。Your app uses these files to keep track of its own state.
  • 文件系统位置中的其他文件,应用在其中声明了访问功能。Other files in locations of the file system where your app has declared capabilities to access. 这些文件通常位于某个 KnownFolders 中。These are most commonly located in one of the KnownFolders.

应用对前两个类别的文件拥有完全控制权,因为这些文件是该应用的包文件的一部分,并由该应用独占访问。Your app has full control on the first two categories of files, because they’re part of your app's package files and are accessed by your app exclusively. 对于最后一个类别中的文件,应用必须注意,其他应用和 OS 服务可能同时正在访问这些文件。For files in the last category, your app must be aware that other apps and OS services may be accessing the files concurrently.

根据具体的应用,对文件的访问因频率而异:Depending on the app, access to the files can vary on frequency:

  • 非常低。Very low. 这些文件通常在应用启动时打开,并在应用挂起时保存。These are usually files that are opened once when the app launches and are saved when the app is suspended.
  • 低。Low. 这些文件是用户专门对其执行操作(例如保存或加载)的文件。These are files that the user is specifically taking an action on (such as save or load).
  • 中等或高。Medium or high. 应用必须在这些文件中持续更新数据(例如,自动保存功能或常量元数据跟踪)。These are files in which the app must constantly update data (for example, autosave features or constant metadata tracking).

在文件大小方面,请考虑 WriteBytesAsync 方法的以下图表中的性能数据。For file size, consider the performance data in the following chart for the WriteBytesAsync method. 该图表比较了在受控的环境中,根据平均性能为每个文件大小 10000 次操作,针对不同的文件大小完成某项操作所需的时间。This chart compares the time to complete an operation vs file size, over an average performance of 10000 operations per file size in a controlled environment.

WriteBytesAsync 性能

此图表中有意省略了 Y 轴上的时间值,因为不同的硬件和配置会生成不同的绝对时间值。The time values on the y-axis are omitted intentionally from this chart because different hardware and configurations will yield different absolute time values. 但是,我们在测试中观察到了这些趋势的一致性:However, we have consistently observed these trends in our tests:

  • 对于极小的文件 (<= 1 MB):完成操作所需的时间一贯很短。For very small files (<= 1 MB): The time to complete the operations is consistently fast.
  • 对于较大的文件 (> 1 MB):完成操作所需的时间开始呈指数级增长。For larger files (> 1 MB): The time to complete the operations starts to increase exponentially.

应用挂起期间的 I/OI/O during app suspension

如果你想要保留状态信息或元数据以便在后续会话中使用,则应用必须能够处理挂起。Your app must designed to handle suspension if you want to keep state information or metadata for use in later sessions. 有关应用挂起的背景信息,请参阅应用生命周期此博客文章For background information about app suspension, see App lifecycle and this blog post.

除非 OS 授权长时间执行应用,否则,当应用挂起时,它可以在 5 秒钟内释放其资源并保存其数据。Unless the OS grants extended execution to your app, when your app is suspended it has 5 seconds to release all its resources and save its data. 为获得最佳可靠性和用户体验,请始终假设处理挂起任务的时间是有限的。For the best reliability and user experience, always assume the time you have to handle suspension tasks is limited. 在处理挂起任务的 5 秒时限内,请记住以下指导原则:Keep in mind the following guidelines during the 5 second time period for handling suspension tasks:

  • 尽量减少 I/O,以避免刷新和释放操作导致争用状态。Try to keep I/O to a minimum to avoid race conditions caused by flushing and release operations.
  • 避免写入需要数百毫秒或更长时间才能写入的文件。Avoid writing files that require hundreds of milliseconds or more to write.
  • 如果应用使用 Write 方法,请记住这些方法需要的所有中间步骤。If your app uses the Write methods, keep in mind all the intermediate steps that these methods require.

如果应用在挂起期间处理少量的状态数据,则大多数情况下,你都可以使用 Write 方法来刷新数据。If your app operates on a small amount of state data during suspension, in most cases you can use the Write methods to flush the data. 但是,如果应用使用大量的状态数据,请考虑使用流来直接存储数据。However, if your app uses a large amount of state data, consider using streams to directly store your data. 这可能有助于减小 Write 方法的事务模型造成的延迟。This can help reduce the delay introduced by the transactional model of the Write methods.

有关示例,请参阅 BasicSuspension 示例。For an example, see the BasicSuspension sample.

其他示例和资源Other examples and resources

下面是适用于特定方案的几个示例和其他资源。Here are several examples and other resources for specific scenarios.

有关重试文件 I/O 的代码示例Code example for retrying file I/O example

以下伪代码示例假设在用户选取要保存的文件后执行写入,演示在这种情况下如何重试写入 (C#):The following is a pseudo-code example on how to retry a write (C#), assuming the write is to be done after the user picks a file for saving:

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.
}

同步对文件的访问权限Synchronize access to the file

使用 .NET 的并行编程博客文章是一个不错的资源,其中提供了有关并行编程的指导。The Parallel Programming with .NET blog is a great resource for guidance about parallel programming. 有关 AsyncReaderWriterLock 的文章专门介绍了如何保留对文件的独占写入访问权限,同时允许进行并发读取访问。In particular, the post about AsyncReaderWriterLock describes how to maintain exclusive access to a file for writes while allowing concurrent read access. 请注意,序列化 I/O 会影响性能。Keep in mind that serializing I/O will impact performance.

另请参阅See also