Golangはチャネルの同期待機グループWaitGroupを使用して同時クローラーを開発します

Goのコンカレントプログラミングには定型的な言い回しがあります。共有メモリを使用して通信しないでください。共有通信を使用してメモリを共有してください。

Go言語は、ロックを使用してさまざまなGoroutineの共有状態を保護し、情報を共有する(共有メモリで通信する)ことを推奨していません。むしろ、(通信でメモリを共有するために)チャネルを介して各Goroutine間で共有状態または共有状態の変更を転送することを奨励します。これにより、1つのGoroutineのみがロックと同時に共有状態にアクセスすることも保証できます。

もちろん、主流のプログラミング言語では、複数のスレッド間でのデータ共有のセキュリティと一貫性を保証するために、ロック、条件変数、アトミック操作などの同期ツールの基本セットが提供されます。Go言語標準ライブラリもこれらの同期メカニズムを提供しています。使用方法は他の言語と似ています。

 

 

WaitGroup

WaitGroup、同期待機グループ。

タイプに関しては、構造です。WaitGroupの目的は、goroutineコレクションの実行が完了するのを待つことです。メインのゴルーチンはAdd()メソッドを呼び出して、待機するゴルーチンの数を設定します。次に、各goroutineが実行され、実行が完了した後にDone()メソッドが呼び出されます。同時に、Wait()メソッドを使用して、すべてのゴルーチンが実行されるまでブロックできます。

Add()メソッド

このメソッドを使用して、カウンター値をWaitGroupに設定します。各同期グループには、この同期待機グループで実行されるgoroutinの数を示すカウンターがあることがわかります。

カウンターの値が0になった場合、待機中にブロックされたゴルーチンが解放されていることを意味し、カウンターの値が負の場合、パニックを引き起こし、プログラムはエラーを報告します。

Done()メソッド

Done()メソッドは、WaitGroupがグループ内のgoroutineの実行を同期的に待機するときに、このWaitGroupのカウンター値から1を引いた値を設定します。

待機()メソッド

Wait()メソッドは、現在のゴルーチンが待機し、ブロッキング状態になることを意味します。WaitGroupのカウンターがゼロになるまで。ブロックを解除するために、goroutineは実行を継続できます。

サンプルコード

 
  1. 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)
        }
    }

チャンネルチャンネル

チャネルはGoroutinesの通信チャネルと見なすことができます。パイプの一端から他端への水の流れと同様に、データは一端から他端へ送信され、チャネルを通じて受信されます。

先にGo言語の同時実行性について説明したとき、複数のGoroutineが共有データを実装する場合、従来の同期メカニズムも提供しますが、Go言語はGoroutine間でチャネルチャネルを使用することを強くお勧めします。コミュニケーション。

「共有メモリを介して通信するのではなく、通信を介してメモリを共有する」これは、golangコミュニティを普及させた古典的なフレーズです

送受信する

データを送受信するチャネルは、デフォルトでブロックされています。データの一部がチャネルに送信されると、別のGoroutineがチャネルからデータを読み取るまで、データはsendステートメントでブロックされます。対照的に、チャネルからデータを読み取る場合、ゴルーチンがチャネルにデータを書き込むまで読み取りはブロックされます。

サンプルコード:次のコードはスリープを追加します。これにより、チャネルブロッキングをよりよく理解できます。

 
  1. 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チャネルを作成しました。次に、サブゴルーチンを開始し、ループで10個の数値を出力しました。次に、チャネルtrueに入力trueを書き込みます。
次に、メインのゴルーチンで、ch1からデータを読み取ります。このコード行はブロックされます。つまり、子のGoroutineがチャネルにデータを書き込むまで、メインのゴルーチンは次のコード行まで実行されません。

したがって、チャネルを介してサブゴルーチンとメインゴルーチン間の通信を実現できます。子のゴルーチンが実行されると、メインのゴルーチンはch1のデータを読み取ることによってブロックされます。これにより、サブゴルーチンが最初に実行されます。これは時間の必要性を排除します。

前のプログラムでは、メインゴルーチンが終了しないように、メインゴルーチンをスリープ状態にしています。WaitGroupを使用して、メインのゴルーチンが終了する前にサブゴルーチンが実行されるようにします。

デッドロック

チャネルを使用するときに考慮すべき重要な要素はデッドロックです。Goroutineが1つのチャネルでデータを送信する場合、他のGoroutineがデータを受信する必要があります。これが発生しない場合、プログラムは実行中にデッドロックします。

同様に、Goroutineがチャネルからのデータの受信を待機している場合、一部のGoroutineがチャネルにデータを書き込みます。そうでない場合、プログラムはデッドロックします。

サンプルコード

 
  1. package main

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

エラー:

 
  1. 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は、実際に同時に実行されるエンティティーです。その最下層はコルーチンを使用して並行性を実現します。コルーチンはユーザーモードで実行されるユーザースレッドです。greenthreadと同様に、コルーチンを選択するgo最下層の開始点は、次の特性があるためです。 :

ユーザースペースは、カーネルモードとユーザーモードの切り替えによって発生するコストを回避します。
言語およびフレームワークレイヤーによってスケジュールできます。
スタックスペースが小さいほど、多数のインスタンスを作成できます。

Goroutineスケジューラ

並行スケジューリングを実行する:GPMモデル

オペレーティングシステムによって提供されるカーネルスレッドに加えて、Goは独自の2レベルのスレッドモデルを構築しました。goroutineメカニズムは、M:Nのスレッドモデルを実装します。goroutineメカニズムは、コルーチンの実装です。Golangの組み込みスケジューラにより、マルチコアCPUの各CPUがコルーチンを実行できます。

上記のコンテンツはhttps://github.com/rubyhan1314/Golang-100-Daysからのもので、 
主に同期待機グループとチャネルの基本的な使用法、およびgoが並行性を処理する方法を説明しています。詳細については、上記を引き続き参照できます。チュートリアル。

戦闘爬虫類

私は以前に多くのことを言ったことがありますが、これはこのスクリプトの準備のためだけであり、それ以外の場合は急すぎます。
ここでは、チャネルを使用して同時実行を行うクローラースクリプトを作成しました。同期待機グループがあり、()操作を実行しています。

コードを直接見る

HTMLを取得

 
  1. 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
    }

Webページを.htmlファイルとしてクロールする

 
  1. 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 + " 抓取成功"

    }

クロールの方法を書き終えて、重要な部分にたどり着きました

ワーカー関数を定義する

 
  1. 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 退出 。。。。。")
    }

チャネルからデータを取得する

 
  1. 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)
        //}
    }

クロール結果を送信する

 
  1. 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)

        }
    }

一般的な考え方はこれです:

2つのチャネルを定義したことが
わかります。1つはURLを格納するために使用され、もう1つはクロール結果を格納するために使用されます。バッファスペースは100です。メソッドdoWorkでは、sendResultは結果チャネルanonymousの出力の待機をブロックします関数は、ページチャネルの出力を待つことです。

200のURLをページチャネルに書き込むとすぐに、匿名関数がページの出力を取得し、htmlをクロールする関数であるasyn_worker関数を実行します(結果チャネルに保存します)。

次に、sendResult関数は結果チャネルの出力を取得し、結果を出力します

匿名機能では20個のゴロションが同時に存在し、同期待ちグループをパラメーターとして有効にしており、理論上はマシンの性能に応じて同時実行数を定義することができます。

主な機能

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

        var wg sync.WaitGroup

        doWork(1,200, &wg)
        //输出执行时间,单位为毫秒。
        fmt.Printf("执行时间: %ds",(time.Now().UnixNano() - start_time) / 1000)

    }

クローラーを実行して実行時間を計算します。この時間はマシンごとに異なりますが、それほど異なることはありません

完全なコード

 
  1. 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言語のロックのみを使用することを明確にすることを目的としています。目的は、重要なリソースのセキュリティ問題を防ぐことです。

チャネルとgoroutineにより、Goの並行プログラミングは非常に簡単で安全になり、プログラマーはビジネスに集中して開発効率を向上させることができます。

https://gzky.live/article/Golang%E9%80%9A%E9%81%93%E5%90%8C%E6%AD%A5%E7%AD%89%E5%BE%85%に移動しますE7%BB%84%20%E5%B9%B6%E5%8F%91%E7%88%AC%E8%99%AB

公開された23元の記事 ウォンの賞賛2 ビュー5236

おすすめ

転載: blog.csdn.net/bianlitongcn/article/details/105367100