本文可協助您解決 I/O 的預設行為是同步的問題,但顯示為異步。
原始產品版本: Windows
原始 KB 編號: 156932
摘要
Microsoft Windows 上的檔案 I/O 可以是同步或異步的。 I/O 的預設行為是同步的,其中會呼叫 I/O 函式,並在 I/O 完成時傳回。 異步 I/O 允許 I/O 函式立即將執行傳回給呼叫端,但在未來的某個時間之前不假設 I/O 完成。 操作系統會在 I/O 完成時通知呼叫端。 相反地,呼叫端可以使用操作系統的服務來判斷未完成 I/O 作業的狀態。
異步 I/O 的優點是呼叫端有時間在 I/O 作業完成時執行其他工作或發出更多要求。 「重疊 I/O」一詞經常用於指異步 I/O,而「非重疊 I/O」則用於指同步 I/O。 本文使用 I/O 作業的異步和同步詞彙。 本文假設讀者已熟悉檔案 I/O 函式,例如 CreateFile、 ReadFileWriteFile。
異步 I/O 作業通常就像同步 I/O 一樣。 本文會在後續各節中討論的某些條件,讓 I/O 作業以同步方式完成。 呼叫端沒有時間進行背景工作,因為 I/O 函式直到 I/O 完成後才會傳回。
數個函式與同步和異步 I/O 相關。 本文使用 ReadFile 和 WriteFile 作為範例。 良好的替代機制是 ReadFileEx 與 WriteFileEx。 雖然本文只討論磁碟 I/O,但許多原則可以套用至其他類型的 I/O,例如序列 I/O 或網路 I/O。
設定異步輸入輸出
在開啟檔案時,必須指定FILE_FLAG_OVERLAPPED標誌在CreateFile中。 此旗標可讓檔案上的 I/O 作業以異步方式完成。 以下是範例:
HANDLE hFile;
hFile = CreateFile(szFileName,
GENERIC_READ,
0,
NULL,
OPEN_EXISTING,
FILE_FLAG_NORMAL | FILE_FLAG_OVERLAPPED,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
ErrorOpeningFile();
當您撰寫異步 I/O 的程式代碼時請小心,因為系統會保留在需要時同步作業的權利。 因此,如果您撰寫程式以正確處理可能以同步或異步方式完成的 I/O 作業,則最好這麼做。 範例程式代碼示範此考慮。
程式在等候異步操作完成時可以執行許多工作,例如佇列其他作業,或執行背景工作。 例如,下列程式碼會正確處理讀取作業的重疊和非重疊完成。 它只不過等候未完成的 I/O 結束。
if (!ReadFile(hFile,
pDataBuf,
dwSizeOfBuffer,
&NumberOfBytesRead,
&osReadOperation )
{
if (GetLastError() != ERROR_IO_PENDING)
{
// Some other error occurred while reading the file.
ErrorReadingFile();
ExitProcess(0);
}
else
// Operation has been queued and
// will complete in the future.
fOverlapped = TRUE;
}
else
// Operation has completed immediately.
fOverlapped = FALSE;
if (fOverlapped)
{
// Wait for the operation to complete before continuing.
// You could do some background work if you wanted to.
if (GetOverlappedResult( hFile,
&osReadOperation,
&NumberOfBytesTransferred,
TRUE))
ReadHasCompleted(NumberOfBytesTransferred);
else
// Operation has completed, but it failed.
ErrorReadingFile();
}
else
ReadHasCompleted(NumberOfBytesRead);
注意
&NumberOfBytesRead 傳入 ReadFile 與 &NumberOfBytesTransferred 傳入 GetOverlappedResult 不同。 如果作業已進行異步處理,則會 GetOverlappedResult 用來判斷作業完成之後在作業中傳輸的實際位元組數目。 傳遞到&NumberOfBytesRead中的ReadFile是毫無意義的。
另一方面,如果作業立即完成,則 &NumberOfBytesRead 傳入 ReadFile 的位元組數目是有效的。 在此情況下,忽略傳入OVERLAPPED的ReadFile結構,不可與GetOverlappedResult或WaitForSingleObject一起使用。
另一個關於異步操作的警告是,您必須等到其待處理作業完成後,才可以使用 OVERLAPPED 結構。 換句話說,如果您有三個未處理的 I/O 作業,則必須使用三 OVERLAPPED 個結構。 如果您重複使用 OVERLAPPED 結構,將會在I/O作業中收到無法預期的結果,而且您可能會遇到數據損毀的情況。 此外,您必須正確初始化它,以避免任何剩餘數據影響到新作業,這樣才能在第一次使用 OVERLAPPED 結構時,或在先前作業完成後再次使用它。
相同的限制類型適用於作業中使用的數據緩衝區。 在對應的 I/O 作業完成之前,數據緩衝區不得讀取或寫入;讀取或寫入緩衝區可能會導致錯誤和損毀的數據。
異步 I/O 仍顯示為同步
不過,如果您依照本文稍早的指示進行操作,所有 I/O 作業通常仍會按照發出的順序以同步方式完成,並且沒有任何ReadFile作業會傳回 GetLastError(),這意味著您沒有時間進行任何背景工作。 為什麼會發生此情況?
即使您已針對異步操作撰寫程式代碼,I/O 作業仍會同步完成的原因有很多。
壓縮
異步操作的一個阻礙是新技術文件系統 (NTFS) 壓縮。 檔系統驅動程式不會以異步方式存取壓縮檔;而是將所有作業設為同步。 此阻礙不適用於使用類似 COMPRESS 或 PKZIP 之公用程式壓縮的檔案。
NTFS 加密
類似於壓縮,檔案加密會導致系統驅動程式將異步 I/O 轉換成同步。 如果檔案已解密,I/O 要求將會是異步的。
擴充檔案
I/O 作業同步完成的另一個原因是作業本身。 在 Windows 上,任何擴充其長度的檔案寫入作業都會是同步的。
注意
應用程式可以透過使用SetFileValidData函式來變更檔案的有效數據長度,然後發出WriteFile,以非同步方式執行先前提到的寫入操作。
使用 SetFileValidData (可在 Windows XP 和更新版本上使用),應用程式可以有效率地擴充檔案,而不會造成零填滿檔案的效能損失。
由於 NTFS 檔案系統不會將數據以零填充至由 SetFileValidData 定義的有效數據長度(VDL),此函式具有安全性影響,其中檔案有可能會獲得先前由其他檔案使用的叢集。 因此, SetFileValidData 要求呼叫端已啟用新的 SeManageVolumePrivilege 功能(根據預設,這隻會指派給系統管理員)。 Microsoft建議獨立軟體供應商(ISV)仔細考慮使用這類功能的影響。
緩存
大部分的 I/O 驅動程式(磁碟、通訊和其他驅動程式)都有特殊案例程式代碼,其中,如果可以立即完成 I/O 要求,作業將會完成,或 ReadFileWriteFile 函式會傳回 TRUE。 以各種方式,這些類型的作業看起來是同步的。 對於磁碟裝置,一般而言,當資料已快取在記憶體中後,可以立即完成 I/O 要求。
數據不在快取中
不過,如果數據不在快取中,快取方案可能對您不利。 Windows 快取是透過檔案映射在內部實作的。 Windows 中的記憶體管理員不提供非同步頁面錯誤機制,以管理快取管理員使用的檔案對應。 快取管理員可以驗證要求的頁面是否在記憶體中,因此,如果您發出非同步快取讀取,而頁面不在記憶體中,文件系統驅動程式會假設您不希望封鎖線程,而該要求將由有限的工作線程集區處理。 呼叫 ReadFile 後,控制權會被傳回給您的程式,但讀取操作仍在等待中。
這在少數請求下運作良好,但由於工作線程池的數量有限(目前的 16 MB 記憶體系統上為三個),因此在特定時間排隊等待磁碟驅動程式的請求仍然只有少數。 如果您針對不在快取中的數據發出許多 I/O 作業,快取管理員和記憶體管理員就會變成飽和,而且您的要求會同步處理。
快取管理員的行為也可以根據您循序或隨機存取檔案而受到影響。 在檔案循序存取時,快取的優勢最為明顯。
FILE_FLAG_SEQUENTIAL_SCAN呼叫中的CreateFile旗標會優化此類型存取的快取。 不過,如果您以隨機方式存取檔案,請使用 FILE_FLAG_RANDOM_ACCESS 中的 CreateFile 旗標,指示快取管理員優化其隨機存取行為。
請勿使用快取
FILE_FLAG_NO_BUFFERING 旗標對檔案系統的行為具有最大影響,在執行異步操作時。 這是保證 I/O 要求為非同步的最佳方式。 它會指示文件系統完全不使用任何快取機制。
注意
使用此旗標有一些限制,這與數據緩衝區對齊和裝置的扇區大小相關。 如需詳細資訊,請參閱 CreateFile 函式文件中關於正確使用此旗標的函式參考。
真實世界測試結果
以下是範例程式代碼中的一些測試結果。 數字的規模在這裡並不重要,並且視不同的計算機而異,但數字彼此之間的關係揭示出旗標對效能的一般影響。
您可以預期會看到類似下列其中一項的結果:
測試 1
Asynchronous, unbuffered I/O: asynchio /f*.dat /n Operations completed out of the order in which they were requested. 500 requests queued in 0.224264 second. 500 requests completed in 4.982481 seconds.這項測試示範先前提到的程式會快速發出 500 個 I/O 要求,而且有太多時間執行其他工作或發出更多要求。
測試 2
Synchronous, unbuffered I/O: asynchio /f*.dat /s /n Operations completed in the order issued. 500 requests queued and completed in 4.495806 seconds.此測試示範此程式花了 4.495880 秒呼叫 ReadFile 來完成其作業,但測試 1 只花費了 0.224264 秒來發出相同的要求。 在測試 2 中,程式沒有任何額外的時間可以執行任何背景工作。
測試 3
Asynchronous, buffered I/O: asynchio /f*.dat Operations completed in the order issued. 500 requests issued and completed in 0.251670 second.此測試展示快取的同步特性。 所有讀取於 0.251670 秒內發出和完成。 換句話說,異步請求最終是以同步的方式完成。 此測試也會示範數據在快取中時快取管理員的高效能。
測試 4
Synchronous, buffered I/O: asynchio /f*.dat /s Operations completed in the order issued. 500 requests and completed in 0.217011 seconds.此測試示範與測試 3 中相同的結果。 從快取進行的同步讀取比異步讀取稍微快一些。 此測試也會示範數據在快取中時快取管理員的高效能。
結論
您可以決定哪一種方法是最佳方法,因為它全都取決於程式所執行的作業類型、大小和數目。
預設的檔案存取若未指定任何特殊旗標CreateFile,則為同步和快取作業。
注意
您會在此模式中獲得一些自動化的非同步行為,因為檔案系統驅動程式會進行預測性非同步預讀,以及非同步延遲寫入已修改的資料。 雖然此行為不會讓應用程式的 I/O 異步,但它是絕大多數簡單應用程式的理想案例。
另一方面,如果您的應用程式並不簡單,您可能必須執行一些分析與效能監視來判斷最佳方法,類似於本文稍早所述的測試。 分析在 ReadFile 或 WriteFile 函式中花費的時間,然後將這段時間與實際 I/O 作業完成所需的時間進行比較,是非常有用的。 如果大部分時間都花在實際發出 I/O 中,則您的 I/O 會以同步方式完成。 如果發出 I/O 要求所花費的時間相較於 I/O 作業完成所需的時間相對較小,則您的作業會以異步方式處理。 本文稍早所述的範例程式代碼會使用 函 QueryPerformanceCounter 式來執行自己的內部分析。
效能監視有助於判斷程式使用磁碟和快取的效率。 追蹤 Cache 物件的任何性能計數器將會指出快取管理員的效能。 追蹤實體磁碟或邏輯磁碟物件的性能計數器將會指出磁碟系統的效能。
有數個公用程式有助於效能監視。
PerfMon 和 DiskPerf 特別有用。 若要讓系統收集磁碟系統效能的數據,您必須先發出 DiskPerf 命令。 發出命令之後,您必須重新啟動系統以啟動資料收集。