golang實現併發爬蟲三(用佇列排程器實現)

公子若不胖天下誰胖發表於2020-04-24

欲看此文,必先可先看:

golang實現併發爬蟲一(單任務版本爬蟲功能)

gollang實現併發爬蟲二(簡單排程器)

 

上文中的用簡單的排程器實現了併發爬蟲。

並且,也提到了這種併發爬蟲的實現可以提高爬取效率。

當workerCount為1和workerCount為10時其爬取效率是有明顯不同的。

然而,文末其實也提到了這個簡單排程器實現的爬蟲有個不可控或者說是控制力太小了的問題。

究其原因就是因為我們實現的方法是來一個request就給建立一個groutine。

為了讓這個程式變得更為可控。得想想怎麼可以優化下了。

 

現在,非常明顯,優化點就是我不想要來一個request就建立一個這個實現過程。

那麼,我們可以想到佇列。

把request放到request佇列裡。

那麼,request佇列裡一定是會有一個request的頭的,我們就可以把這個request的頭元素給到worker去做實現。

也就是這樣:

 

 

 but,這樣是沒有對worker進行一個控制的。

我們希望request可以選擇我們想要的一個worker。

那麼,我們也可以讓scheduler維護一個worker的佇列。

 

 這裡用了三個並行的模組:

1.engine 引擎模組。

2.scheduler 排程器模組。

3.worker 工作模組。

這三者通訊都是通過channel來通訊的。

 

上圖中可知道排程器模組實際上是維護了2個channel,一個是request的channel,一個是worker的channel。

//佇列排程器
//這個scheduler與engine和worker之間的通訊都是通過channel來連線的。
//故爾它的肚子裡應該有request相關的channel和worker相關的channel.
//另外注意這裡worker的channel的型別是chan Request。
type QueuedScheduler struct {
    requestChan chan con_engine.Request
    workerChan  chan chan con_engine.Request
}

那麼,我們就只需要在這個scheduler排程器的兩個channel裡,各取一個元素,即取request和worker(chan con_engine.Request),把request發給worker就可以了。

一直不斷的去取和傳送,這就是這個佇列排程器要做的事情了。

那個彎曲的箭頭也就是指的這個事情了。在request的佇列裡找到合適的request發給worker佇列裡合適的worker就好。

這就是一個整體的思想了。

 

稍微說下關於維護如何兩個佇列的程式碼。

重點在於怎麼才能做到各讀取一個元素。

channel的讀取是會阻塞的。

如果我先讀取request,如果讀取不到,那麼在等待的時候就沒有辦法取到worker了。

解決方案就是用select,因為select會保證一點,select裡的每一個case都會被執行到且會很快速的執行。

func (s *QueuedScheduler) Run() {
    s.requestChan = make(chan con_engine.Request) //指標接收者才能改變裡面的內容。
    s.workerChan = make(chan chan con_engine.Request)
    go func() {
        var requestQ []con_engine.Request
        var workerQ []chan con_engine.Request
        for {
            var activeRequest con_engine.Request
            var activeWorker chan con_engine.Request
            if len(requestQ) > 0 && len(workerQ) > 0 {
                activeRequest = requestQ[0]
                activeWorker = workerQ[0]
            }
            //收到一個request就讓request排隊,收到一個worker就讓worker排隊。所有的channel操作都放到select裡。
            select {
            case r := <-s.requestChan:
                requestQ = append(requestQ, r)
            case w := <-s.workerChan:
                workerQ = append(workerQ, w)
            case activeWorker <- activeRequest:
                requestQ = requestQ[1:]
                workerQ = workerQ[1:]
            }
        }
    }()
}

select就是在做三件事情:

1.從requestChan裡收一個request,將這個request存在變數requestQ裡。

2.從workerChan裡收一個worker,將這個worker存在變數workerQ裡。

3.把第一個requestQ裡的第一個元素髮給第一個workerQ裡的第一個元素。  

 

其他程式碼就感興趣的同學自己看吧。

作者就先說到這裡。

總體排程的思想上面的圖中。

具體的實現在原始碼裡。

歡迎大家留言指教。

 

原始碼:

https://github.com/anmutu/du_crawler/tree/master/04crawler

 

相關文章