python iocp_记对协程增加IOCP支持时候踩过的一些坑
之前在對tbox的協程庫中增加了基于IOCP的io處理,期間踩了不少的坑,這邊就做個簡單記錄吧,省的到時候忘記了,自己看不懂自己這個代碼 (= =)
坑點一
WSARecv/WSASend在lpNumberOfBytesRecv和overlap同時被設置的情況下,如果它們的io操作立即返回成功,并且lpNumberOfBytesRecv里面也已經獲取到了實際的io處理字節數,但是io event還會被放到完成隊列,需要調用GetQueuedCompletionStatus去獲取。
之前就被這個坑了很久,我原本想既然如果WASRecv已經立即完成,那么沒必要再去通過GetQueuedCompletionStatus等待了,我可以直接快速返回處理結果,減少不必要的協程等待和切換。
然而經過實測發現,實時并非如此,即時我這次立即從recv返回出去不再等待了,等到下次recv如果是pending狀態的話,有可能等待到的是上次處理成功的io event,蛋疼。。
為了進一步了解其機制,我翻了下官方文檔,里面對lpNumberOfBytesRecvd有如下說明:
lpNumberOfBytesRecvd
A pointer to the number, in bytes, of data received by this call if the receive operation completes immediately.
Use NULL for this parameter if the lpOverlapped parameter is not NULL to avoid potentially erroneous results. This parameter can be NULL only if the lpOverlapped parameter is not NULL.
大概得意思就是告訴我們,如果lpNumberOfBytesRecvd和lpOverlapped同時被設置,可能會有潛在的問題(估計多半就是我遇到的坑吧),所以看文檔這意思,估計是建議我們在傳入lpOverlapped的情況下,盡量不要設置lpNumberOfBytesRecvd,傳NULL就好。
但是這不是我想要的結果,我還是希望能夠快速處理WSARecv立即成功返回的情況,沒必要每次都切換協程去等待io event。
一種辦法是調用兩次WSARecv,一次不傳lpOverlapped,直接嘗試讀取,如果成功立即返回結果,讀不到的話,再通過lpOverlapped送入完成隊列,去隊列化等待事件完成。
另外一種辦法就是每次GetQueuedCompletionStatus的時候去忽略之前已經成功處理和返回的時間對象。
不過這兩種我試了下,都不是很理想,處理起來比較復雜,效率也不高,那有沒有其他更好的辦法呢,我翻了下golang源碼中對于iocp的處理,終于找到了解決辦法。(果然還是golang給力)
// This package uses the SetFileCompletionNotificationModes Windows
// API to skip calling GetQueuedCompletionStatus if an IO operation
// completes synchronously. There is a known bug where
// SetFileCompletionNotificationModes crashes on some systems (see
// https://support.microsoft.com/kb/2568167 for details).
var useSetFileCompletionNotificationModes bool // determines is SetFileCompletionNotificationModes is present and safe to use
原來golang是用了SetFileCompletionNotificationModes API去設置iocp端口在每次GetQueuedCompletionStatus等待io事件的時候,去直接內部忽略已經立即成功返回的io事件,也就是說如果WSARecv如果立即成功返回,那么不會再隊列化io event了。
這個好呀,正是我想要的東西,而且用起來也很簡單,只需要:
SetFileCompletionNotificationModes(socket, FILE_SKIP_COMPLETION_PORT_ON_SUCCESS);
不過這個接口,并不是所有win系統版本都支持,xp上就沒法這么用了,不過好在現在用xp的也不多了,對于檢測此接口的支持情況,golang也有相關實現,可以直接參考:
// checkSetFileCompletionNotificationModes verifies that
// SetFileCompletionNotificationModes Windows API is present
// on the system and is safe to use.
// See https://support.microsoft.com/kb/2568167 for details.
func checkSetFileCompletionNotificationModes() {
err := syscall.LoadSetFileCompletionNotificationModes()
if err != nil {
return
}
protos := [2]int32{syscall.IPPROTO_TCP, 0}
var buf [32]syscall.WSAProtocolInfo
len := uint32(unsafe.Sizeof(buf))
n, err := syscall.WSAEnumProtocols(&protos[0], &buf[0], &len)
if err != nil {
return
}
for i := int32(0); i < n; i++ {
if buf[i].ServiceFlags1&syscall.XP1_IFS_HANDLES == 0 {
return
}
}
useSetFileCompletionNotificationModes = true
}
這個就不細說了,大家自己看看代碼都能明白,不過似乎通過SetFileCompletionNotificationModes設置忽略io event的方式,對于udp處理上有其他問題,具體我也沒驗證過,既然golang沒將其用到udp上,我也就暫時只對tcp上這么處理了,具體可以看下下面的注釋說明:
if pollable && useSetFileCompletionNotificationModes {
// We do not use events, so we can skip them always.
flags := uint8(syscall.FILE_SKIP_SET_EVENT_ON_HANDLE)
// It's not safe to skip completion notifications for UDP:
// https://blogs.technet.com/b/winserverperformance/archive/2008/06/26/designing-applications-for-high-performance-part-iii.aspx
if net == "tcp" {
flags |= syscall.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS
}
err := syscall.SetFileCompletionNotificationModes(fd.Sysfd, flags)
if err == nil && flags&syscall.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS != 0 {
fd.skipSyncNotif = true
}
}
坑點二
GetQueuedCompletionStatusEx如果每次只等待到一個io event的情況下,效率比GetQueuedCompletionStatus慢了接近一倍。
由于GetQueuedCompletionStatusEx每次可同時等待n個io事件,可以極大減少調用次數,快速處理多個時間對象。
對于同時有多個事件完成的情況下,這個調用確實比使用GetQueuedCompletionStatus效率好很多,但是我在本地做壓測的時候發現,如果每次只有一個io事件完成的情況下,GetQueuedCompletionStatusEx的效率真的很差,還不如GetQueuedCompletionStatus了。
為此,我在tbox的iocp處理里面稍微做了下優化:
/* we can use GetQueuedCompletionStatusEx() to increase performance, perhaps,
* but we may end up lowering perf if you max out only one I/O thread.
*/
tb_long_t wait = -1;
if (poller->lastwait_count > 1 && poller->func.GetQueuedCompletionStatusEx)
wait = tb_poller_iocp_event_wait_ex(poller, func, timeout);
else wait = tb_poller_iocp_event_wait(poller, func, timeout);
// save the last wait count
poller->lastwait_count = wait;
我記錄了下最近一次的等待時間返回數,如果最近都是只有一次io事件的話,那么切換到GetQueuedCompletionStatus去等待io,如果當前io事件比較多的話,再切換到GetQueuedCompletionStatusEx去處理。
這里我目前只是簡單處理了下,測試下來效果還不錯,等后續有時間可以根據實際效果,再調整下優化策略。
坑點三
CancelIO只能用于取消當前線程投遞的io事件,想要在取消其他線程投遞的io事件,需要使用CancelIOEx,這個大家應該都知道,我就簡單提下好了。
坑點四
寫好IOCP程序,還是很不容易的,各種處理細節核注意事項非常多,這里暫時不一一列舉了,等有時間我再補充下吧,大家也可以通過評論,貼下自己平常開發IOCP程序時候,經常遇到的一些坑點。
總結
以上是生活随笔為你收集整理的python iocp_记对协程增加IOCP支持时候踩过的一些坑的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python写快排_python 实现快
- 下一篇: 文本相似度计算python lda_如何