071-并发爬虫

Posted --Allen--

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了071-并发爬虫相关的知识,希望对你有一定的参考价值。

学习从来都不是一件困难的事情。那为啥我们学习会如此痛苦?其实难在坚持。这就好比跑步并不是一件困难的事,但是难在十年如一日的坚持。

如果你仔细一点,会发现我的 csdn 头像是《海贼王》这部动漫的主角头像。海贼王这部动漫从 1997 年开始连载,至今已经连载 21 年了。将一部作品连载至今,需要的不仅仅是智慧,更多的是毅力。

如果我们学习也能如此,日复一日,年复一年,我想也应该能有所成就吧。

废话不多说,我们接上一篇的话题,如何控制 goroutine 的并发度呢?总的来说,有两种办法,待会你会看到。

1. 使用 channel 控制并发度

上一篇我们写的代码本身没有错,但错在无穷无尽的并发会对系统造成影响,这会耗尽系统的文件描述符。一个解决办法就是控制最高并发度。

我们的程序从 url “池子”每拿到一个 url 就开启一个 goroutine,如果池子里的 url 数量非常大,一个不小心就能开启上万个 goroutine,我们希望能得到控制。

  • 使用 channel 占位控制并发度

假设我们限制 20 并发,怎么做?一个简单的做法就是每开启一个 goroutine 前,就向 channel 里放入一个占位标记,当 goroutine 运行结束后,就把占位标记移除。由于 channel 的缓冲区是固定的,一旦 channel 被占满,就再也无法开启新的 goroutine,除非有旧的 goutine 运行结束,并将 channel 中的标记删除。伪代码如下:

// tokens 是一个大小为 20 的 channel
tokens := make(chan struct, 20)
for 
    tokens <- struct
    go f()
    <-tokens

上面的程序就能控制同时最多 20 个 goroutine 运行。

  • 使用固定数量的 long-lived goroutine 控制并发度

另一种控制并发度的方法,是使用 long-lived goroutine,即长时间存活的 goroutine(简称长活协程),这有点像我们以前常说的线程池,在这里你可以说叫协程池。伪代码如下:

tasks := make(chan Type)
for i := 0; i < 20; i++ 
    go func() 
        for task := tasks 
            run(task)
        
    

有经验的同学一看就能知道,这是一个 producter-consumer 模型,即生产者消费者模型。生产者源源不断的将待执行的任务丢入缓冲区 tasks,而消费者(我们开启的 20 个 long-lived goroutine) 源源不断的消费缓冲区的任务。

上面这两种方法各有千秋,下面是具体的程序。

2. 程序

下面的两份代码都在 gopl/goroutine/concurrence 目录下面。

2.1 使用 channel 控制并发

package main

import (
    "fmt"
    "gopl/goroutine/link"
    "log"
    "os"
)

var tokens = make(chan struct, 20)

func crawl(url string) []string 
    fmt.Println(url)
    // 占位
    tokens <- struct
    urls, err := link.ExtractLinks(url)
    // 移除占位标记
    <-tokens
    if err != nil 
        log.Print(fmt.Sprintf("\\x1b[31m%v\\x1b[0m", err))
    
    return urls


func main() 
    if len(os.Args) < 2 
        fmt.Println("Usage:\\n\\tgo run crawl.go <url>")
        os.Exit(1)
    

    workList := make(chan []string)
    seen := make(map[string]bool)

    var n int
    n++
    go func() 
        workList <- os.Args[1:]
    ()

    for ; n > 0; n-- 
        list := <-workList
        for _, url := range list 
            if seen[url] 
                continue
            
            n++
            seen[url] = true
            go func(url string) 
                workList <- crawl(url)
            (url)
        
    

2.2 使用 long-lived goroutine

package main

import (
    "fmt"
    "gopl/goroutine/link"
    "log"
    "os"
)

func crawl(url string) []string 
    fmt.Println(url)
    urls, err := link.ExtractLinks(url)
    if err != nil 
        log.Print(fmt.Sprintf("\\x1b[31m%v\\x1b[0m", err))
    
    return urls


func main() 
    if len(os.Args) < 2 
        fmt.Println("Usage:\\n\\tgo run crawl.go <url>")
        os.Exit(1)
    

    workList := make(chan []string)
    unseenLinks := make(chan string)
    seen := make(map[string]bool)

    var n int
    n++
    go func() 
        workList <- os.Args[1:]
    ()

    // 开启 20 个固定的 long-lived goroutine
    for i := 0; i < 20; i++ 
        go func() 
            for url := range unseenLinks 
                urls := crawl(url)
                go func()  workList <- urls ()
            
        ()
    

    for list := range workList 
        for _, url := range list 
            if seen[url] 
                continue
            
            unseenLinks <- url
        
    

如此一来,你就可以再次运行上面的代码,就不会出现之前的 too many open files 的问题了。运行方法还是和上一篇一样,赶紧试试吧。

3. 总结

  • 掌握控制并发度的方法

以上是关于071-并发爬虫的主要内容,如果未能解决你的问题,请参考以下文章

python 之 并发编程(进程池与线程池同步异步阻塞非阻塞线程queue)

python之进程池与线程池

go并发-对象池实现

go并发-对象池实现

go并发-对象池实现

go并发-对象池实现