Golang 通道,同步等待組 并發爬蟲

Golang:通道,同步等待組 并發爬蟲

在Go的并發編程中有一句很經典的話:不要以共享內存的方式去通信,而要以通信的方式去共享內存。

在Go語言中并不鼓勵用鎖保護共享狀態的方式在不同的Goroutine中分享信息(以共享內存的方式去通信)。而是鼓勵通過channel將共享狀態或共享狀態的變化在各個Goroutine之間傳遞(以通信的方式去共享內存),這樣同樣能像用鎖一樣保證在同一的時間只有一個Goroutine訪問共享狀態。

當然,在主流的編程語言中為了保證多線程之間共享數據安全性和一致性,都會提供一套基本的同步工具集,如鎖,條件變量,原子操作等等。Go語言標準庫也毫不意外的提供了這些同步機制,使用方式也和其他語言也差不多。

image

WaitGroup

WaitGroup,同步等待組。

在類型上,它是一個結構體。一個WaitGroup的用途是等待一個goroutine的集合執行完成。主goroutine調用了Add()方法來設置要等待的goroutine的數量。然后,每個goroutine都會執行并且執行完成后調用Done()這個方法。與此同時,可以使用Wait()方法來阻塞,直到所有的goroutine都執行完成。

Add()方法

Add這個方法,用來設置到WaitGroup的計數器的值。我們可以理解為每個waitgroup中都有一個計數器 用來表示這個同步等待組中要執行的goroutin的數量。

如果計數器的數值變為0,那么就表示等待時被阻塞的goroutine都被釋放,如果計數器的數值為負數,那么就會引發恐慌,程序就報錯了。

Done()方法

Done()方法,就是當WaitGroup同步等待組中的某個goroutine執行完畢后,設置這個WaitGroup的counter數值減1。

Wait()方法

Wait()方法,表示讓當前的goroutine等待,進入阻塞狀態。一直到WaitGroup的計數器為零。才能解除阻塞, 這個goroutine才能繼續執行。

示例代碼


package main

import (
    "fmt"
    "sync"
)
var wg sync.WaitGroup // 創建同步等待組對象
func main()  {
    /*
    WaitGroup:同步等待組
        可以使用Add(),設置等待組中要 執行的子goroutine的數量,
        
        在main 函數中,使用wait(),讓主程序處于等待狀態。直到等待組中子程序執行完畢。解除阻塞

        子gorotuine對應的函數中。wg.Done(),用于讓等待組中的子程序的數量減1
     */
    //設置等待組中,要執行的goroutine的數量
    wg.Add(2)
    go fun1()
    go fun2()
    fmt.Println("main進入阻塞狀態。。。等待wg中的子goroutine結束。。")
    wg.Wait() //表示main goroutine進入等待,意味著阻塞
    fmt.Println("main,解除阻塞。。")

}
func fun1()  {
    for i:=1;i<=10;i++{
        fmt.Println("fun1.。。i:",i)
    }
    wg.Done() //給wg等待中的執行的goroutine數量減1.同Add(-1)
}
func fun2()  {
    defer wg.Done()
    for j:=1;j<=10;j++{
        fmt.Println("\tfun2..j,",j)
    }
}

channel通道

通道可以被認為是Goroutines通信的管道。類似于管道中的水從一端到另一端的流動,數據可以從一端發送到另一端,通過通道接收。

在前面講Go語言的并發時候,我們就說過,當多個Goroutine想實現共享數據的時候,雖然也提供了傳統的同步機制,但是Go語言強烈建議的是使用Channel通道來實現Goroutines之間的通信。

“不要通過共享內存來通信,而應該通過通信來共享內存” 這是一句風靡golang社區的經典語

接收和發送

一個通道發送和接收數據,默認是阻塞的。當一個數據被發送到通道時,在發送語句中被阻塞,直到另一個Goroutine從該通道讀取數據。相對地,當從通道讀取數據時,讀取被阻塞,直到一個Goroutine將數據寫入該通道。

示例代碼:以下代碼加入了睡眠,可以更好的理解channel的阻塞

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan int)
    done := make(chan bool) // 通道
    go func() {
        fmt.Println("子goroutine執行。。。")
        time.Sleep(3 * time.Second)
        data := <-ch1 // 從通道中讀取數據
        fmt.Println("data:", data)
        done <- true
    }()
    // 向通道中寫數據。。
    time.Sleep(5 * time.Second)
    ch1 <- 100

    <-done
    fmt.Println("main。。over")

}

在上面的程序中,我們先創建了一個chan bool通道。然后啟動了一條子Goroutine,并循環打印10個數字。然后我們向通道ch1中寫入輸入true。
然后在主goroutine中,我們從ch1中讀取數據。這一行代碼是阻塞的,這意味著在子Goroutine將數據寫入到該通道之前,主goroutine將不會執行到下一行代碼。

因此,我們可以通過channel實現子goroutine和主goroutine之間的通信。當子goroutine執行完畢前,主goroutine會因為讀取ch1中的數據而阻塞。從而保證了子goroutine會先執行完畢。這就消除了對時間的需求。

在之前的程序中,我們要么讓主goroutine進入睡眠,以防止主要的Goroutine退出。要么通過WaitGroup來保證子goroutine先執行完畢,主goroutine才結束。

死鎖

使用通道時要考慮的一個重要因素是死鎖。如果Goroutine在一個通道上發送數據,那么預計其他的Goroutine應該接收數據。如果這種情況不發生,那么程序將在運行時出現死鎖。

類似地,如果Goroutine正在等待從通道接收數據,那么另一些Goroutine將會在該通道上寫入數據,否則程序將會死鎖。

示例代碼
package main

func main() {  
    ch := make(chan int)
    ch <- 5
}
報錯:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    /Users/ruby/go/src/l_goroutine/demo08_chan.go:5 +0x50

Goroutine

Goroutine 是實際并發執行的實體,它底層是使用協程(coroutine)實現并發,coroutine是一種運行在用戶態的用戶線程,類似于 greenthread,go底層選擇使用coroutine的出發點是因為,它具有以下特點:

用戶空間 避免了內核態和用戶態的切換導致的成本
可以由語言和框架層進行調度
更小的??臻g允許創建大量的實例

Goroutine 調度器

Go并發調度: G-P-M模型

在操作系統提供的內核線程之上,Go搭建了一個特有的兩級線程模型。goroutine機制實現了M : N的線程模型,goroutine機制是協程(coroutine)的一種實現,golang內置的調度器,可以讓多核CPU中每個CPU執行一個協程。

以上內容來自 https://github.com/rubyhan1314/Golang-100-Days
主要說明一下同步等待組和通道的基本使用,以及 go 是如何處理并發的,更多可以繼續參考以上,來自千峰的 go 教程。

實戰爬蟲

前面說了這么多只不過是為這個腳本做鋪墊,要不然則來的太唐突。
我這里寫了一個爬蟲腳本,用到了通道來做并發,并有同步等待組做 awit() 操作

直接來看代碼

獲取html
func HttpGet(url string) (result string, err error) {
    resp, err1 := http.Get(url)
    if err != nil {
        err = err1
        return
    }
    defer resp.Body.Close()
    //讀取網頁的body內容
    buf := make([]byte, 4*1024)
    for true {
        n, err := resp.Body.Read(buf)
        if err != nil {
            if err == io.EOF{
                break
            }else {
                fmt.Println("resp.Body.Read err = ", err)
                break
            }
        }
        result += string(buf[:n])
    }
    return
}
爬取網頁存為 .html 文件
func spiderPage(url string) string {

    fmt.Println("正在爬取", url)
    //爬,將所有的網頁內容爬取下來
    result, err := HttpGet(url)
    if err != nil {
        fmt.Println(err)
    }
    //把內容寫入到文件
    filename := strconv.Itoa(rand.Int()) + ".html"
    f, err1 := os.Create(filename)
    if err1 != nil{
        fmt.Println(err1)
    }
    //寫內容
    f.WriteString(result)
    //關閉文件
    f.Close()
    return url + " 抓取成功"

}

爬取方法方面就寫完了,接下來就到了重要的部分了

定義一個工作者函數
func doWork(start, end int,wg *sync.WaitGroup) {
    fmt.Printf("正在爬取第%d頁到%d頁\n", start, end)
    //因為很有可能爬蟲還沒有結束下面的循環就已經結束了,所以這里就需要且到通道
    page := make(chan string,100)
    results := make(chan string,100)


    go sendResult(results,start,end)

    go func() {

        for i := 0; i <= 20; i++ {
            wg.Add(1)
            go asyn_worker(page, results, wg)
        }
    }()

    for i := start; i <= end; i++ {
            url := "https://tieba.baidu.com/f?kw=%E7%BB%9D%E5%9C%B0%E6%B1%82%E7%94%9F&ie=utf-8&pn=" + strconv.Itoa((i-1)*50)
            page <- url
            println("加入" + url + "到page")
        }
        println("關閉通道")
        close(page)

    wg.Wait()
    //time.Sleep(time.Second * 5)
    println(" Main 退出 。。。。。")
}
從通道取出數據
func asyn_worker(page chan string, results chan string,wg *sync.WaitGroup){

    defer wg.Done()  //defer wg.Done()必須放在go并發函數內

    for{
        v, ok := <- page //顯示的調用close方法關閉通道。
        if !ok{
            fmt.Println("已經讀取了所有的數據,", ok)
            break
        }
        //fmt.Println("取出數據:",v, ok)
        results <- spiderPage(v)
    }


    //for n := range page {
    //  results <- spiderPage(n)
    //}
}
發送抓取結果
func sendResult(results chan string,start,end int)  {

    //for i := start; i <= end; i++ {
    //  fmt.Println(<-results)
    //}

    // 發送抓取結果
    for{
        v, ok := <- results
        if !ok{
            fmt.Println("已經讀取了所有的數據,", ok)
            break
        }
        fmt.Println(v)

    }
}

大體思路是這樣的:

可以看到我定義了兩個通道,一個是用來存入 url 的,另一個是用來存入爬取結果的,緩沖空間是 100
在方法 doWork 中, sendResult 會阻塞等待 results 通道的輸出,匿名函數則是等待 page 通道的輸出

緊接著下面就是把 200 個 url 寫入 page 通道,匿名函數得到 page 的輸出就會執行 asyn_worker 函數,也就是爬取 html 的函數了(將其存入results 通道)

然后 sendResult 函數得到 results 通道的輸出,將結果打印出來

可以看到 我在匿名函數中并發了 20 個 goroution,并且啟用了同步等待組作為參數傳入,理論上可以根據機器的性能來定義 并發數

main函數

func main() {
    start_time := time.Now().UnixNano()

    var wg sync.WaitGroup

    doWork(1,200, &wg)
    //輸出執行時間,單位為毫秒。
    fmt.Printf("執行時間: %ds",(time.Now().UnixNano() - start_time) / 1000)

}

運行爬蟲并計算運行時間,這個時間因機器而異,但應該不會相差太多

完整代碼

package main

import (
    "fmt"
    "io"
    "sync"
    "math/rand"
    "net/http"
    "os"
    "strconv"
    "time"
)



func HttpGet(url string) (result string, err error) {
    resp, err1 := http.Get(url)
    if err != nil {
        err = err1
        return
    }
    defer resp.Body.Close()
    //讀取網頁的body內容
    buf := make([]byte, 4*1024)
    for true {
        n, err := resp.Body.Read(buf)
        if err != nil {
            if err == io.EOF{
                break
            }else {
                fmt.Println("resp.Body.Read err = ", err)
                break
            }
        }
        result += string(buf[:n])
    }
    return
}


//爬取網頁
func spiderPage(url string) string {

    fmt.Println("正在爬取", url)
    //爬,將所有的網頁內容爬取下來
    result, err := HttpGet(url)
    if err != nil {
        fmt.Println(err)
    }
    //把內容寫入到文件
    filename := strconv.Itoa(rand.Int()) + ".html"
    f, err1 := os.Create(filename)
    if err1 != nil{
        fmt.Println(err1)
    }
    //寫內容
    f.WriteString(result)
    //關閉文件
    f.Close()
    return url + " 抓取成功"

}

func asyn_worker(page chan string, results chan string,wg *sync.WaitGroup){

    defer wg.Done()  //defer wg.Done()必須放在go并發函數內

    for{
        v, ok := <- page //顯示的調用close方法關閉通道。
        if !ok{
            fmt.Println("已經讀取了所有的數據,", ok)
            break
        }
        //fmt.Println("取出數據:",v, ok)
        results <- spiderPage(v)
    }

    //for n := range page {
    //  results <- spiderPage(n)
    //}
}

func doWork(start, end int,wg *sync.WaitGroup) {
    fmt.Printf("正在爬取第%d頁到%d頁\n", start, end)
    //因為很有可能爬蟲還沒有結束下面的循環就已經結束了,所以這里就需要且到通道
    page := make(chan string,100)
    results := make(chan string,100)


    go sendResult(results,start,end)

    go func() {

        for i := 0; i <= 20; i++ {
            wg.Add(1)
            go asyn_worker(page, results, wg)
        }
    }()


    for i := start; i <= end; i++ {
            url := "https://tieba.baidu.com/f?kw=%E7%BB%9D%E5%9C%B0%E6%B1%82%E7%94%9F&ie=utf-8&pn=" + strconv.Itoa((i-1)*50)
            page <- url
            println("加入" + url + "到page")
        }
        println("關閉通道")
        close(page)

    wg.Wait()
    //time.Sleep(time.Second * 5)
    println(" Main 退出 。。。。。")
}


func sendResult(results chan string,start,end int)  {

    //for i := start; i <= end; i++ {
    //  fmt.Println(<-results)
    //}

    // 發送抓取結果
    for{
        v, ok := <- results
        if !ok{
            fmt.Println("已經讀取了所有的數據,", ok)
            break
        }
        fmt.Println(v)

    }
}

func main() {
    start_time := time.Now().UnixNano()

    var wg sync.WaitGroup

    doWork(1,200, &wg)
    //輸出執行時間,單位為毫秒。
    fmt.Printf("執行時間: %ds",(time.Now().UnixNano() - start_time) / 1000)

}

總體來說,這個腳本就是為了弄清楚 Go 語言的并發原理 以及 通道,同步等待組的基本使用,或者只用 go 語言的鎖,目的都是為了防止 臨界資源的安全問題。

有了 channel 和 goroutine 之后,Go 的并發編程變得異常容易和安全,得以讓程序員把注意力留到業務上去,實現開發效率的提升。

歡迎轉載,但要聲明出處,不然我順著網線過去就是一拳。
個人技術博客:http://www.gzky.live

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容

  • 參考《快學 Go 語言》第 11 課 —— 千軍萬馬跑協程《快學 Go 語言》第 12 課 —— 通道let's ...
    合肥黑閱讀 2,933評論 2 7
  • 開發go程序的時候,時常需要使用goroutine并發處理任務,有時候這些goroutine是相互獨立的,而有的時...
    駐馬聽雪閱讀 2,463評論 0 21
  • 控制并發有三種種經典的方式,一種是通過channel通知實現并發控制 一種是WaitGroup,另外一種就是Con...
    wiseAaron閱讀 10,685評論 4 34
  • 不期望幸運順利的人對別人的苦難波折感同身受,但也希望別高高在上鄙視身心磨難的他人是矯情。哎呀你太矯情了,不就是懷個...
    viviv閱讀 428評論 0 0
  • 昨天的泰興市場開拓相當順利。有此我想到很多。 其實我這輩子做任何事情都比較順利,那不是因為我的能力有多強,...
    悅琴1818閱讀 149評論 0 0