重要的應用程式介面
開發人員有時會在使用 FileIO 和 PathIO 類別的 Write 方法來執行檔案系統 I/O 作業時遇到一組常見問題。 例如,常見問題包括:
- 檔案已部分寫入。
- 呼叫其中一個方法時,應用程式會收到例外狀況。
- 操作會留下檔名類似於目標檔案的 .TMP 檔案。
FileIO 和 PathIO 類別的 Write 方法包括:
- WriteBufferAsync
- WriteBytesAsync
- WriteLinesAsync
- WriteTextAsync
本文提供這些方法運作方式的詳細數據,讓開發人員更瞭解何時及如何使用它們。 本文提供指導方針,且不會嘗試為所有可能的檔案 I/O 問題提供解決方案。
備註
本文著重於範例和討論中的 FileIO 方法。 不過, PathIO 方法遵循類似的模式,本文中的大部分指引也適用於這些方法。
便利性與控制
StorageFile 物件不是像是原生 Win32 程式設計模型的檔案句柄。 相反地,StorageFile 是檔案的表徵,具有操控其內容的方法。
使用 StorageFile 執行 I/O 時,瞭解此概念很有用。 例如,寫入檔案部分介紹了三種寫入檔案的方法:
- 使用 FileIO.WriteTextAsync 方法。
- 藉由建立緩衝區,然後呼叫 FileIO.WriteBufferAsync 方法。
- 使用資料流的四步驟模型:
- 開啟 檔案以取得數據流。
- 取得輸出數據流。
- 建立 DataWriter 物件,並呼叫對應的 寫入 方法。
認可 數據寫入器中的數據,並在輸出數據流 排清。
前兩個案例是應用程式最常使用的案例。 在一次性操作中寫入檔案會更容易編寫和維護,並且也可以減輕應用程式處理許多檔案 I/O 複雜性的責任。 不過,這種便利性帶來的代價是失去對整個作業的控制,以及無法在特定點精確捕捉錯誤的能力。
交易式模型
FileIO 和 PathIO 類別的 Write 方法將上述第三個寫入模型中的步驟進行封裝,並增加一個額外的層。 此層被封裝在儲存交易中。
若要保護源檔的完整性,以防寫入數據時發生問題, Write 方法會使用交易式模型,方法是使用 OpenTransactedWriteAsync 開啟檔案。 此程式會建立 StorageStreamTransaction 物件。 在建立此交易對象之後,API 會以類似 檔案存取 範例或 StorageStreamTransaction 文章中的程式代碼範例的方式,將資料寫入。
下圖說明 WriteTextAsync 方法在成功寫入作業中執行的基礎工作。 此圖提供作業的簡化檢視。 例如,它會略過不同線程上的文字編碼和異步完成等步驟。
使用 Write 方法、FileIO 和 PathIO 類別的優點是,它們可以取代使用數據流的更複雜的四步驟模型:
- 一個 API 呼叫來處理所有中繼步驟,包括錯誤。
- 如果發生問題,則會保留源檔。
- 系統狀態會盡量保持乾淨。
不過,由於有這麼多可能的中繼失敗點,失敗的機會會增加。 發生錯誤時,可能很難了解進程失敗的位置。 下列各節說明使用 Write 方法並提供可能的解決方案時,可能會遇到的一些失敗。
FileIO 和 PathIO 類別之 Write 方法的常見錯誤碼
下表提供應用程式開發人員在使用 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 | 這可能會因為多個未處理的 I/O 作業或大型檔案大小而發生。 | 藉由控制數據流,更細微的方法可能會解決錯誤。 |
E_FAIL (0x80004005) | 任意 | 其他 | 重試操作。 如果仍然失敗,可能是平臺錯誤,而且應用程式應該因為處於不一致的狀態而終止。 |
可能導致錯誤之檔案狀態的其他考慮
除了 Write 方法所返回的錯誤之外,以下是應用程式在寫入檔案時可以預期的一些指導原則。
只有在作業完成時,才會將數據寫入檔案
當寫入作業進行時,您的應用程式不應該對檔案中的數據進行任何假設。 嘗試在作業完成之前存取檔案可能會導致數據不一致。 您的應用程式應該負責追蹤尚未完成的 I/O 作業。
讀者
如果正在被寫入的檔案同時也被符合規範的讀取器使用(也就是說,以 FileAccessMode.Read開啟),後續讀取將會失敗,並出現錯誤ERROR_OPLOCK_HANDLE_CLOSED(0x80070323)。 有時候,當 寫入 作業進行時,應用程式會重試開啟檔案以供讀取。 這可能會導致一個競爭條件,即在嘗試覆寫原始檔案時,寫入最終失敗,因為原始檔案無法被取代。
來自 KnownFolders 的檔案
您的應用程式可能不是唯一嘗試存取位於任何 KnownFolders 上的檔案的應用程式。 不保證如果作業成功,應用程式寫入檔案的內容會在下次嘗試讀取檔案時維持不變。 此外,在此案例中,共用或拒絕存取錯誤變得更加常見。
I/O 衝突 (輸入/輸出衝突)
如果我們的應用程式使用本機數據中檔案的 Write 方法,可能會降低並行錯誤的可能性,但仍需要一些謹慎。 如果同時將多個 寫入 作業傳送至檔案,則無法保證檔案中最終會有哪些數據。 若要解決此問題,我們建議您的應用程式將 的 寫入操作序列化到檔案。
~TMP 檔案
有時候,如果作業被強制取消(例如,如果應用程式被操作系統暫停或終止),則交易可能無法適當地被認可或關閉。 這可能會留下擴展名為 (.~TMP) 的檔案。 處理應用程式啟用時,請考慮刪除這些暫存盤(如果它們存在於應用程式的本機數據中)。
根據檔案類型的考慮
根據檔案類型、存取錯誤的頻率,以及其檔案大小,某些錯誤可能會變得更加普遍。 一般而言,您的應用程式可以存取的檔案有三種類別:
- 使用者在您應用程式的本機數據資料資料夾中建立和編輯的檔案。 這些只會在使用您的應用程式時建立和編輯,而且它們只存在於應用程式內。
- 應用程式元數據。 您的應用程式會使用這些檔案來追蹤自己的狀態。
- 應用程式已宣告可存取之檔案系統位置的其他檔案。 這些最常位於 KnownFolders 的其中一個 中。
您的應用程式可完全控制前兩個類別的檔案,因為它們是您應用程式的套件檔案的一部分,而且是由您的應用程式獨佔存取。 對於上一個類別中的檔案,您的應用程式必須注意其他應用程式和OS服務可能會同時存取檔案。
視應用程式而定,對檔案的存取可能會因頻率而異:
- 非常低。 這些通常是在應用程式啟動時開啟一次的檔案,並在應用程式暫停時儲存。
- 低。 這些是用戶特別在上採取動作的檔案(例如儲存或載入)。
- 中等或高。 這些檔案是應用程式必須持續更新資料的檔案(例如,自動儲存功能或常數元數據追蹤)。
針對檔案大小,請考慮 WriteBytesAsync 方法的下列圖表中的效能數據。 此圖表會比較完成作業與檔案大小的時間,以及受控制環境中每個檔案大小 10000 個作業的平均效能。
Y 軸上的時間值會刻意從此圖表中省略,因為不同的硬體和組態會產生不同的絕對時間值。 不過,我們在測試中一直觀察到這些趨勢:
- 對於非常小的檔案(<= 1 MB):完成作業的時間始終很快。
- 對於較大的檔案 (> 1 MB):完成作業的時間會開始以指數方式增加。
應用程式暫停期間的 I/O
如果您想要保留狀態資訊或元數據,以供稍後會話使用,您的應用程式必須設計為處理暫停。 如需應用程式暫停的背景資訊,請參閱 應用程式生命週期 和 此部落格文章。
除非操作系统將延長執行時間授予您的應用程式,否則當應用程式被暫停時,您有 5 秒鐘的時間來釋放其所有資源並儲存其資料。 為了獲得最佳可靠性和用戶體驗,請一律假設您必須處理暫停工作的時間有限。 請記住下列指導方針,在處理暫停工作的 5 秒期間:
- 請嘗試將 I/O 保持在最小值,以避免排清和釋放作業所造成的競爭狀況。
- 避免寫入需要數百毫秒甚至更長時間的檔案。
- 如果您的應用程式使用 Write 方法,請記住這些方法所需的所有中繼步驟。
如果您的 app 在暫停期間對少量的狀態數據運作,在大部分情況下,您可以使用 Write 方法來排清數據。 不過,如果您的應用程式使用大量的狀態數據,請考慮使用數據流直接儲存您的數據。 這有助於減少由 Write 方法的交易模型引入的延遲。
如需範例,請參閱 BasicSuspension 範例。
其他範例和資源
以下是特定案例的數個範例和其他資源。
重試檔案 I/O 範例的程式代碼範例
以下是如何在使用者選擇要儲存的檔案後重試寫入的虛擬代碼範例(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 的描述中說明了如何在允許併發讀取存取的同時,維持對檔案的寫入操作的獨佔存取權。 請記住,串行化 I/O 會影響效能。