python iocp_记对协程增加IOCP支持时候踩过的一些坑
之前在對(duì)tbox的協(xié)程庫(kù)中增加了基于IOCP的io處理,期間踩了不少的坑,這邊就做個(gè)簡(jiǎn)單記錄吧,省的到時(shí)候忘記了,自己看不懂自己這個(gè)代碼 (= =)
坑點(diǎn)一
WSARecv/WSASend在lpNumberOfBytesRecv和overlap同時(shí)被設(shè)置的情況下,如果它們的io操作立即返回成功,并且lpNumberOfBytesRecv里面也已經(jīng)獲取到了實(shí)際的io處理字節(jié)數(shù),但是io event還會(huì)被放到完成隊(duì)列,需要調(diào)用GetQueuedCompletionStatus去獲取。
之前就被這個(gè)坑了很久,我原本想既然如果WASRecv已經(jīng)立即完成,那么沒(méi)必要再去通過(guò)GetQueuedCompletionStatus等待了,我可以直接快速返回處理結(jié)果,減少不必要的協(xié)程等待和切換。
然而經(jīng)過(guò)實(shí)測(cè)發(fā)現(xiàn),實(shí)時(shí)并非如此,即時(shí)我這次立即從recv返回出去不再等待了,等到下次recv如果是pending狀態(tài)的話,有可能等待到的是上次處理成功的io event,蛋疼。。
為了進(jìn)一步了解其機(jī)制,我翻了下官方文檔,里面對(duì)lpNumberOfBytesRecvd有如下說(shuō)明:
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同時(shí)被設(shè)置,可能會(huì)有潛在的問(wèn)題(估計(jì)多半就是我遇到的坑吧),所以看文檔這意思,估計(jì)是建議我們?cè)趥魅雔pOverlapped的情況下,盡量不要設(shè)置lpNumberOfBytesRecvd,傳NULL就好。
但是這不是我想要的結(jié)果,我還是希望能夠快速處理WSARecv立即成功返回的情況,沒(méi)必要每次都切換協(xié)程去等待io event。
一種辦法是調(diào)用兩次WSARecv,一次不傳lpOverlapped,直接嘗試讀取,如果成功立即返回結(jié)果,讀不到的話,再通過(guò)lpOverlapped送入完成隊(duì)列,去隊(duì)列化等待事件完成。
另外一種辦法就是每次GetQueuedCompletionStatus的時(shí)候去忽略之前已經(jīng)成功處理和返回的時(shí)間對(duì)象。
不過(guò)這兩種我試了下,都不是很理想,處理起來(lái)比較復(fù)雜,效率也不高,那有沒(méi)有其他更好的辦法呢,我翻了下golang源碼中對(duì)于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
原來(lái)golang是用了SetFileCompletionNotificationModes API去設(shè)置iocp端口在每次GetQueuedCompletionStatus等待io事件的時(shí)候,去直接內(nèi)部忽略已經(jīng)立即成功返回的io事件,也就是說(shuō)如果WSARecv如果立即成功返回,那么不會(huì)再隊(duì)列化io event了。
這個(gè)好呀,正是我想要的東西,而且用起來(lái)也很簡(jiǎn)單,只需要:
SetFileCompletionNotificationModes(socket, FILE_SKIP_COMPLETION_PORT_ON_SUCCESS);
不過(guò)這個(gè)接口,并不是所有win系統(tǒng)版本都支持,xp上就沒(méi)法這么用了,不過(guò)好在現(xiàn)在用xp的也不多了,對(duì)于檢測(cè)此接口的支持情況,golang也有相關(guān)實(shí)現(xiàn),可以直接參考:
// 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
}
這個(gè)就不細(xì)說(shuō)了,大家自己看看代碼都能明白,不過(guò)似乎通過(guò)SetFileCompletionNotificationModes設(shè)置忽略io event的方式,對(duì)于udp處理上有其他問(wèn)題,具體我也沒(méi)驗(yàn)證過(guò),既然golang沒(méi)將其用到udp上,我也就暫時(shí)只對(duì)tcp上這么處理了,具體可以看下下面的注釋說(shuō)明:
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
}
}
坑點(diǎn)二
GetQueuedCompletionStatusEx如果每次只等待到一個(gè)io event的情況下,效率比GetQueuedCompletionStatus慢了接近一倍。
由于GetQueuedCompletionStatusEx每次可同時(shí)等待n個(gè)io事件,可以極大減少調(diào)用次數(shù),快速處理多個(gè)時(shí)間對(duì)象。
對(duì)于同時(shí)有多個(gè)事件完成的情況下,這個(gè)調(diào)用確實(shí)比使用GetQueuedCompletionStatus效率好很多,但是我在本地做壓測(cè)的時(shí)候發(fā)現(xiàn),如果每次只有一個(gè)io事件完成的情況下,GetQueuedCompletionStatusEx的效率真的很差,還不如GetQueuedCompletionStatus了。
為此,我在tbox的iocp處理里面稍微做了下優(yōu)化:
/* 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;
我記錄了下最近一次的等待時(shí)間返回?cái)?shù),如果最近都是只有一次io事件的話,那么切換到GetQueuedCompletionStatus去等待io,如果當(dāng)前io事件比較多的話,再切換到GetQueuedCompletionStatusEx去處理。
這里我目前只是簡(jiǎn)單處理了下,測(cè)試下來(lái)效果還不錯(cuò),等后續(xù)有時(shí)間可以根據(jù)實(shí)際效果,再調(diào)整下優(yōu)化策略。
坑點(diǎn)三
CancelIO只能用于取消當(dāng)前線程投遞的io事件,想要在取消其他線程投遞的io事件,需要使用CancelIOEx,這個(gè)大家應(yīng)該都知道,我就簡(jiǎn)單提下好了。
坑點(diǎn)四
寫(xiě)好IOCP程序,還是很不容易的,各種處理細(xì)節(jié)核注意事項(xiàng)非常多,這里暫時(shí)不一一列舉了,等有時(shí)間我再補(bǔ)充下吧,大家也可以通過(guò)評(píng)論,貼下自己平常開(kāi)發(fā)IOCP程序時(shí)候,經(jīng)常遇到的一些坑點(diǎn)。
總結(jié)
以上是生活随笔為你收集整理的python iocp_记对协程增加IOCP支持时候踩过的一些坑的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: python写快排_python 实现快
- 下一篇: websocket python爬虫_p