Golang:通道,同步等待組 并發爬蟲
在Go的并發編程中有一句很經典的話:不要以共享內存的方式去通信,而要以通信的方式去共享內存。
在Go語言中并不鼓勵用鎖保護共享狀態的方式在不同的Goroutine中分享信息(以共享內存的方式去通信)。而是鼓勵通過channel將共享狀態或共享狀態的變化在各個Goroutine之間傳遞(以通信的方式去共享內存),這樣同樣能像用鎖一樣保證在同一的時間只有一個Goroutine訪問共享狀態。
當然,在主流的編程語言中為了保證多線程之間共享數據安全性和一致性,都會提供一套基本的同步工具集,如鎖,條件變量,原子操作等等。Go語言標準庫也毫不意外的提供了這些同步機制,使用方式也和其他語言也差不多。
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