眾所皆知Go是沒有原生的Thread Pool這種東西,這在寫Crawler的時候會造成一些小小的不便。比方說你想要crawl PTT,這樣打一打大概2秒內DDoS protection就把你擋起來了:

docUrlList, _ := GetDocUrlList() //拿到某個版的文章列表
for _, docUrl := range docUrlList {
	go func() {
		docUrl := docUrl
		p, _ := ParseSingleRawDocument(docUrl)
		parseChannel <- p
	}()
}

比較多人會開始尋求限制go concurrency數量的解決方案,就我所知Concurrency Limiter是一個滿多人用的方案,簡單優雅好用:

limit := limiter.NewConcurrencyLimiter(5)
for _, docUrl := range docUrlList {
	limit.Execute(func() {
		docUrl := docUrl
		p, _ := ParseSingleRawDocument(docUrl)
		parseChannel <- p
	})
}

這做法其實滿簡單暴力的,看他的程式碼就可以發現,他其實就是利用一個大小等同於限制數量的buffered channel,利用Buffered Channel已經滿的情況下再往內塞就會堵塞的特性,達成limit的做法。這做法其實一點問題都沒有,limiter本來定義上就是這樣工作的,但是limiter其實有個很大的缺點,就是造成整個流程塞在上面的第三行,同時也會使得Print Log或者UI等等都很難工作。

所以,畢竟Limiter不是thread pool,他會卡在limit.Execute ,而且他的wait也有一些問題,以下是limiter 的原始碼

// Wait will block all the previously Executed jobs completed running.
//
// IMPORTANT: calling the Wait function while keep calling Execute leads to
//            un-desired race conditions
func (c *ConcurrencyLimiter) Wait() {
	for i := 0; i < c.limit; i++ {
		_ = <-c.tickets
	}
}

其實這個Wait問題也很大,首先是他自己註解就有提到的data race的風險,再來就是前面就有提過, limit.Execute() 本身就會blocking,這也就是說這個 Wait 假設放到上面範例code第8行以後,他進入的時間點其實是 當limit.Execute()已經不會阻擋時(剩餘工作量 < limit),才會執行到Wait。所以Wait()到第8行中間要是還需要做什麼事情,他被執行的時機可能會出乎你意料之外。下面我就舉個例子:

limit := limiter.NewConcurrencyLimiter(5)
for _, docUrl := range docUrlList {
	limit.Execute(func() {
		docUrl := docUrl
		p, _ := ParseSingleRawDocument(docUrl)
		parseChannel <- p
	})
}
DoSomething() //假設len(docUrlList) == 200,DoSomething要等到Parse 195條以後才會被call起來。
limit.Wait()

假設docUrl有200筆,那請問DoSomething()啥時會被執行呢?答案是Doc被Parse195筆以後,我們才會執行到這個。這種問題多半是對於limiter用途沒有全面了解就把他當thread pool用的常見的問題,尤其是假設本來用下面的code,很無腦的把他改成Limiter的話,更是如此:

for _, docUrl := range docUrlList {
	go func() {
		docUrl := docUrl
		p, _ := ParseSingleRawDocument(docUrl)
		parseChannel <- p
	}() 
}
DoSomething() //在這裏,DoSomething會被馬上call起來。

所以其實,在很多的情況下,Limiter並不是一個很好的解決方案,我們會在下一章聊聊怎麼用固定數量的Worker來優雅的解決這問題 :沒原生Thread Pool,那我們自製Worker Group

2 Comments

  1. Pingback: 沒原生Thread Pool,那我們自製Worker Group - Fox Nest

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *