多线程与多进程爬虫

  1. 1、线程与进程
    1. 定义的不同
    2. 区别
      1. 进程
      2. 线程
      3. 协程
  • 2、多线程
    1. 2.1 Threading 模块
    2. 2.2 线程池
  • 3、多进程
    1. 3.1 multiprocessing 模块
    2. 3.2 进程池
  • 4、综合案例
  • 1、线程与进程

    几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每一个运行中的程序就是一个进程。当一个程序运行时,内部可能包含多个顺序执行流,每一个顺序执行流就是一个线程。所以注意区分多进程与多线程的概念与不同。

    一个程序,多个执行流——多线程

    • 进程,能够完成多任务,比如 在一台电脑上能够同时运行多个 QQ
    • 线程,能够完成多任务,比如 一个 QQ 中的多个聊天窗口

    定义的不同

    • 进程是系统进行资源分配和调度的一个独立单位.
    • 线程是进程的一个实体, 是 CPU 调度和分派的基本单位, 它是比进程更小的能独立运行的基本单位. 线程自己基本上不拥有系统资源, 只拥有一点在运行中必不可少的资源(如程序计数器, 一组寄存器和栈), 但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
    • 在硬件层面是没有协程的概念,作业片时间

    区别

    • 一个程序至少有一个进程, 一个进程至少有一个线程.

    • 线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。

    • 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率

    • 线程不能够独立执行,必须依存在进程中

    关于多进程和多线程,教科书上最经典的一句话是“进程是资源分配的最小单位,线程是 CPU 调度的最小单位”。这句话应付考试基本上够了,但如果在工作中遇到类似的选择问题,那就没有那么简单了,选的不好,会让你深受其害。所以他也是面试者最喜欢考察的题目之一。

    我们按照多个不同的维度,来看看多进程和多线程的对比(注:都是相对的,不是说一个好得不得了,另一个差的无法忍受)

    维度 多进程 多线程 总结
    数据共享、同步 数据是分开的: 共享复杂,需要用 IPC; 同步简单 多线程共享进程数据:共享简单;同步复杂 各有优势
    内存、CPU 占用内存多,切换复杂,CPU 利用率低 占用内存少,切换简单,CPU 利用率高 线程占优
    创建销毁、切换 创建销毁、切换复杂,速度慢 创建销毁、切换简单,速度快 线程占优
    编程调试 编程简单,调试简单 编程复杂,调试复杂 进程占优
    可靠性 进程间不会相互影响 一个线程挂掉将导致整个进程挂掉 进程占优
    分布式 适应于多核、多机分布 ;如果一台机器不够,扩展到多台机器比较简单 适应于多核分布 进程占优

    然后我们来看下线程和进程间的比较

    多进程 多线程
    优点 内存隔离,单进程已成不会导致整个应用崩溃。方便调试 提高系统的并发性,并且开销小
    缺点 进程间调用,通讯和切换开销均比多线程大 没有内存隔离,单线程的崩溃会导致整个应用的推出,发生内存 bug 时,定位及其不方便(回调噩梦)
    使用场景 目标子功能交互少,如果资源和性能许可,请设计由多个子应用程序来组合完成。 存在大量 IO、网络等耗时操作,或需要与用户交互时,使用多线程有利于提高系统的并行性和用户界面交互的体验。

    1)需要频繁创建销毁的优先用线程。
    实例:web 服务器。来一个建立一个线程,断了就销毁线程。要是用进程,创建和销毁的代价是很难承受的。
    2)需要进行大量计算的优先使用线程。
    所谓大量计算,当然就是要消耗很多 cpu,切换频繁了,这种情况先线程是最合适的。
    实例:图像处理、算法处理
    3)强相关的处理用线程,若相关的处理用进程。
    什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。
    一般的 server 需要完成如下任务:消息收发和消息处理。消息收发和消息处理就是弱相关的任务,而消息处理里面可能又分为消息解码、业务处理,这两个任务相对来说相关性就要强多了。因此消息收发和消息处理可以分进程设计,消息解码和业务处理可以分线程设计。
    4)可能扩展到多机分布的用进程,多核分布的用线程。
    5)都满足需求的情况下,用你最熟悉、最拿手的方式。

    至于”数据共享、同步“、“编程、调试”、“可靠性”这几个维度的所谓的“复杂、简单”应该怎么取舍,只能说:没有明确的选择方法。一般有一个选择原则:如果多进程和多线程都能够满足要求,那么选择你最熟悉、最拿手的那个。

    买了一台服务器 2 核 4 线程 部署一个博客项目 2G 内存

    python 开发的应用 一个进程一个线程 同一时刻只能处理一个请求 并发数只有 1

    并发 项目部署启动 6(2 进程 *(1+2 线程)) 并发数就是 6 线程开的越多 会消耗内存

    线程并发有先后顺序,并行同时去做

    并行数 最大是 2

    并行 同时做多件事情 一起做。

    进程

    • 不共享任何状态
    • 调度由操作系统完成
    • 有独立的内存空间(上下文切换的时候需要保存栈、cpu 寄存器、虚拟内存、以及打开的相关句柄等信息,开销大)
    • 通讯主要通过信号传递的方式来实现(实现方式有多种,信号量、管道、事件等,通讯都需要过内核,效率低)

    线程

    • 共享变量(解决了通讯麻烦的问题,但是对于变量的访问需要加锁)
    • 调度由操作系统完成
    • 一个进程可以有多个线程,每个线程会共享父进程的资源(创建线程开销占用比进程小很多,可创建的数量也会小很多)
    • 通讯除了可使用进程间通讯的方式,还可以通过共享内存的方式进行通信(通过共享内存通信比通过内核要快很多)
    • 线程的使用会给系统带来上下文切换的额外负担。

    协程

    • 调度完全由用户控制
    • 一个线程(进程)可以有多个协程
    • 每个线程(进程)循环按照指定的任务清单顺序完成不同的任务(当任务被堵塞时,执行下一个任务;当恢复时,再回来执行这个任务;任务间切换只需要保存任务的上下文,没有内核的开销,可以不加锁的访问全局变量)
    • 协程需要保证是非堵塞的且没有相互依赖
    • 协程基本上不能同步通讯,多采用异步的消息通讯,效率比较高

    2、多线程

    2.1 Threading 模块

    python 的 thread 模块是底层的模块,python 的 threading 模块是对 thread 做了一些包装的,可以更加方便的被使用

    当一个进程启动之后,会默认产生一个主线程,因为线程是程序执行流的最小单元,当设置多线程时,主线程会创建多个子线程,在 python 中,默认情况下(其实就是 setDaemon(False)),主线程执行完自己的任务以后,就退出了,此时子线程会继续执行自己的任务,直到自己的任务结束。

    import requests
    import re
    import threading
    import time
    
    def download_one_page(url, name):
        print(name,url)
        response = requests.get(url)
        response.encoding = response.apparent_encoding
        html = response.text
        text = re.findall('<div id="content" class="showtxt">(.*?)</div>', html, re.S)
        print(text)
    
    start_time=time.time()
    response = requests.get('http://www.shuquge.com/txt/8659/index.html')
    response.encoding = response.apparent_encoding
    html = response.text
    result = re.findall('<dd><a href="(.*?)">(.*?)</a></dd>', html)
    
    for url, name in result[:5]:   // 每一次循环都启动一个新的线程, 不要同时启动太多了
        host_url='http://www.shuquge.com/txt/8659/'
        t = threading.Thread(target=download_one_page,args=(host_url + url, name))
        t.start()
    
    while len(threading.enumerate()) > 1:    // 判断子线程是否结束
        pass
    print(time.time()-start_time())   // 计算子线程全部走完所花的时间

    2.2 线程池

    为了合理运用内存,我们一般设置每次最多只运行若干个子线程,比如同时要运行 5 个,已经完成了 3 个,为了避免资源浪费,这时候就可以通过线程池进行补调,保证每时每刻都有 5 个线程在同时运行。

    线程池模块:concurrent.futures

    创建它的最简单方法是作为上下文管理器,使用该 with 语句来管理池的创建和销毁。

    import concurrent.futures
    
    if __name__ == "__main__":
        with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
            executor.map(download_one_page, host_url + url, name)

    还可以通过如下方式创建:

    import concurrent.futures
    
    thread_pool=concurrent.futures.ThreadPoolExecutor(max_workers=5) // 设置最多子任务为 5
    
    for url, name in result:  # 每一次循环都启动一个新的线程
        host_url = 'http://www.shuquge.com/txt/8659/'
        thread_pool.submit(download_one_page, host_url + url, name)
    
    thread_pool.shutdown()

    3、多进程

    python GIL 限制了一个程序只能使用一个进程。

    3.1 multiprocessing 模块

    multiprocessing 是 Python 的标准模块,它既可以用来编写多进程,也可以用来编写多线程。multiprocessing 提供了一个 Process 类来代表一个进程对象,这个对象可以理解为是一个独立的进程,可以执行另外的事情。

    import multiprocessing
    import time
    
    def upload():
        print(" 开始上传文件...")
        time.sleep(1)
        print(" 完成上传文件...")
    
    def download():
        print(" 开始下载文件...")
        time.sleep(1)
        print(" 完成下载文件...")
    
    def main():
        multiprocessing.Process(target=upload).start()
        multiprocessing.Process(target=download).start()
    
    if __name__ == '__main__':   // 必须有个主程序
        main()

    还可以这样:

    import multiprocessing
    
    if __name__ == '__main__':    // 必须有个主程序
    
        for url in url_list:
            mp=multiprocessing.Process(target=download_one_page,agrs=(url,))
            mp.start()
    

    3.2 进程池

    也是采用 concurrent.futures 模块。

    import concurrent.futures
    
    process_pool=concurrent.futures.ProcessPoolExecutor(max_workers=5) // 设置最多子任务为 5
    
    for url, name in result:  # 每一次循环都启动一个新的线程
        host_url = 'http://www.shuquge.com/txt/8659/'
        process_pool.submit(download_one_page, host_url+url, name)
    
    process_pool.shutdown()

    4、综合案例

    采用多进程,多线程的方式爬取小说:

    import requests
    import re
    import time
    import concurrent.futures
    
    def get_index():
        response = requests.get('http://www.shuquge.com/txt/8659/index.html')
        response.encoding = response.apparent_encoding
        html = response.text
        result = re.findall('<dd><a href="(.*?)">(.*?)</a></dd>', html)
        return result
    
    # 多线程
    def thread_download_ebook(url, name):
        print(name, url)
        response = requests.get(url)
        response.encoding = response.apparent_encoding
        html = response.text
        result = re.findall('<div id="content" class="showtxt">(.*?)</div>', html, re.S)
        with open(name + '.txt', mode='w', encoding='utf-8') as f:
            f.write(result[0].replace('<br/>&nbsp;&nbsp;&nbsp;&nbsp;', "").replace('<br/>', ""))
    
    # 多进程
    def process_download_ebook(urls):
        # 每一个进程 启动五个线程 25
        thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=5)
        for url, name in urls:
            # 往线程池里面放任务
            thread_pool.submit(thread_download_ebook, 'http://www.shuquge.com/txt/8659/' + url, name)
        # 等待线程池关闭
        thread_pool.shutdown()
    
    if __name__ == '__main__':
        content_list = get_index()[:20]
        length = int(len(content_list)/5)  # 把所有任务分成五份
        start_time = time.time()
        # 启动五个进程
        # 进程之间的相互通信 默认情况下 进程之间的变量不共享数据
        process_pool = concurrent.futures.ProcessPoolExecutor(max_workers=length)
        for i in range(length):
            if i == length:
                i += 1
            process_pool.submit(process_download_ebook, content_list[i * length:(i + 1) * length])
        process_pool.shutdown()
        print(time.time() - start_time)

    欢迎各位看官及技术大佬前来交流指导呀,可以邮件至 jqiange@yeah.net