• python 高级用法 进程、线程和协程


    Prerequisite

    参考文章:廖雪峰的文章【太复杂了】
    参考视频:路飞学城【要钱的】

    概念

    对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个 Word 就启动了一个 Word 进程。
    有些进程还不止同时干一件事,比如 Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。

    单核 CPU 操作系统轮流让各个任务交替执行,任务 1 执行 0.01 秒,切换到任务 2,任务 2 执行 0.01 秒,再切换到任务 3,执行 0.01 秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于 CPU 的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。

    多任务的实现有3种方式:

    • 多进程模式
    • 多线程模式
    • 多进程 + 多线程模式

    线程是最小的执行单元,进程是最小的资源分配单位,而进程由至少一个线程组成;如何调度进程和线程,完全由操作系统决定,程序自己不能决定什么时候执行,执行多长时间。

    协程,又称微线程,作用是在单线程遇到堵塞操作(I/O 操作、发送请求、等待响应)时反复切换任务(任务切换,并非线程切换)

    小笔记

    子程序,或者称为函数,在所有语言中都是层级调用,比如 A 调用 B,B 在执行过程中又调用了 C,C 执行完毕返回,B 执行完毕返回,最后是 A 执行完毕

    一个线程就是执行一个子程序

    默认一个主函数只有一个进程,一个进程默认只有一个线程

    多线程

    #! /usr/bin/env python
    # -*- coding: UTF-8 -*-
    
    from threading import Thread
    import requests
    import time
    
    def get_page(year):
        try:
            response = requests.get('https://www.baidu.com/' + str(year))
        except Exception as e:
            pass
    
    if __name__ == '__main__':
        starttime = time.time()
        for y in range(2000, 2500):
            t = Thread(target=get_page, args=(y, ))
            t.start()
        endtime = time.time()
        print(endtime - starttime) # 多线程,2.27 秒
    
        starttime = time.time()
        for y in range(2000, 2500):
            get_page(y)
        endtime = time.time()
        print(endtime - starttime) # 普通,52 秒
    

    线程池

    在线程池中,仅当全部线程执行完了,才会继续执行下面的代码,这是个大好处

    #! /usr/bin/env python
    # -*- coding: UTF-8 -*-
    
    from concurrent.futures import ThreadPoolExecutor
    import requests
    import time
    
    def get_page(year):
        try:
            response = requests.get('https://www.baidu.com/' + str(year))
        except Exception as e:
            pass
    
    if __name__ == '__main__':
        starttime = time.time()
        with ThreadPoolExecutor(16) as t:
            for y in range(2000, 2500):
                t.submit(get_page, y)
        endtime = time.time()
        print(endtime - starttime) # 线程池,8 秒
    

    多进程

    #! /usr/bin/env python
    # -*- coding: UTF-8 -*-
    
    from multiprocessing import Process
    import requests
    
    def get_page(year):
        try:
            response = requests.get('https://www.baidu.com/' + str(year))
        except Exception as e:
            pass
    
    if __name__ == '__main__':
        p1 = Process(target=get_page, args=("2020", ))
        p2 = Process(target=get_page, args=("2021", ))
        p3 = Process(target=get_page, args=("2022", ))
        p1.start()
        p2.start()
        p3.start()
    

    多进程 + 线程池

    1. 当需要执行多个互相不打扰的函数时,这是个很好的模型,因为一个进程中可以跑多个线程
    #! /usr/bin/env python
    # -*- coding: UTF-8 -*-
    
    from multiprocessing import Queue
    from multiprocessing import Process
    from concurrent.futures import ThreadPoolExecutor
    import requests
    import time
    
    def get_page(year):
        try:
            response = requests.get('https://www.baidu.com/' + str(year))
        except Exception as e:
            pass
    
    def function1(q):
        with ThreadPoolExecutor(16) as t:
            for y in range(2000, 2250):
                t.submit(get_page, y)
    
    def function2(q):
        with ThreadPoolExecutor(16) as t:
            for y in range(2250, 2500):
                t.submit(get_page, y)
    
    if __name__ == '__main__':
        starttime = time.time()
        q = Queue()  # 主进程,没实际作用
        p1 = Process(target=function1, args=(q, ))
        p2 = Process(target=function2, args=(q, ))
        p1.start()
        p2.start()
        p1.join()   # 主进程等待子进程跑完
        p2.join()   # 主进程等待子进程跑完
        endtime = time.time()
        print(endtime - starttime) # 多进程 + 线程池,6 秒
    
    1. 当需要执行多个相互作用的函数时,这也是很好的模型,因为一个主进程中有多个子进程,子进程可以在主线程中相互作用
    #! /usr/bin/env python
    # -*- coding: UTF-8 -*-
    
    from multiprocessing import Queue
    from multiprocessing import Process
    from concurrent.futures import ThreadPoolExecutor
    import requests
    from lxml import etree
    
    def get_img_url(q):
        try:
            for i in range(5):
                response = requests.get(f'https://www.10wallpaper.com/cn/List_wallpapers/page/{str(i)}')
                response.encoding = 'utf-8'
                et = etree.HTML(response.text)
                div_list = et.xpath("//div[@id='pics-list']/p")
                for div in div_list:
                    # 图片 URL
                    href = div.xpath("./a/@href")[0]
                    href = "https://www.10wallpaper.com/wallpaper/medium/2209/" + href.split('.')[0].split('/')[3] + "_medium.jpg"
                    # 图片标题
                    text = div.xpath("./a/span/text()")[0]
                    text = text.replace(',', '-')
                    q.put([href, text]) # 以列表的形式入队
            q.put([None, None])
        except Exception as e:
            pass
    
    def download(q):
        with ThreadPoolExecutor(16) as t:
            while 1:
                img_url, file_name = q.get() # 获取出队 q 传递的值 [图片 URL, 图片标题]
                if(img_url == None):
                    break
                response = requests.get(img_url)
                with open("./img/" + file_name + ".jpg", mode="wb") as f:
                    f.write(response.content)
                print(img_url, file_name) # 打印图片 URL 和图片标题,显示下载进度
    
    if __name__ == '__main__':
        q = Queue()  # 主进程
        p1 = Process(target=get_img_url, args=(q, )) # 获取图片连接
        p2 = Process(target=download, args=(q, )) # 下载图片文件
        p1.start()
        p2.start()
        p1.join()   # 主进程等待子进程跑完
        p2.join()   # 主进程等待子进程跑完
    
    # PS:get_img_url 函数也可以单独分离出一个函数,用线程池运行
    

    协程

    1. 协程的标准写法
    #! /usr/bin/env python
    # -*- coding: UTF-8 -*-
    
    import asyncio
    
    async def task1():
        print("任务1开始")
        await asyncio.sleep(1)
        print("任务1完成")
        return "任务1顺利结束"
    
    async def task2():
        print("任务2开始")
        await asyncio.sleep(2)
        print("任务2完成")
        return "任务2顺利结束"
    
    async def task3():
        print("任务3开始")
        await asyncio.sleep(3)
        print("任务3完成")
        return "任务3顺利结束"
    
    async def main():
        # 任务列表
        tasks = [
            asyncio.create_task(task3()),
            asyncio.create_task(task1()),
            asyncio.create_task(task2()),
        ]
    
        # 不需要返回值,直接运行任务,并等待全部任务跑完
        await asyncio.wait(tasks)
    
        # 读取返回的结果在 result 中,方案一(推荐,按照任务添加的顺序返回结果)
        """
        result = await asyncio.gather(*tasks, return_exceptions=True)
        for r in result:
            print(r)
        """
    
        # 读取返回的结果在 result 中,方案二
        """
        result, pending = await asyncio.wait(tasks)
        for r in result:
            print(r.result())
        """
    
    if __name__ == '__main__':
        # 运行协程,方案一(推荐)
        lop = asyncio.get_event_loop()
        lop.run_until_complete(main())
    
        # 运行协程,方案二
        """
        asyncio.run(main())
        """
    
    1. 协程在爬虫中的运用(以下载图片为例)
    #! /usr/bin/env python
    # -*- coding: UTF-8 -*-
    
    import aiohttp
    import asyncio
    import aiofiles
    
    async def download(url):
        try:
            file_name = url.split("/")[-1]
            # 网络请求
            async with aiohttp.ClientSession() as session:
                async with session.get(url) as resp:
                    # 读取页面源内容(byte)
                    content = await resp.content.read()
                    # 读取网页内容(html)
                    """
                    page_source = await resp.text(encoding='utf-8') # 设置字符集
                    """
                    # 读取 JSON 数据(json)
                    """
                    dic = await resp.json()
                    """
                    # 写入文件
                    async with aiofiles.open(file_name, mode="wb") as f:
                        await f.write(content)
        except Exception as e:
            pass
    
    async def main():
        url_list = [
            "https://www.10wallpaper.com/wallpaper/medium/2209/Brooke_Street_Pier_Travel_Hobart_Tasmania_Australia_5K_medium.jpg",
            "https://www.10wallpaper.com/wallpaper/medium/2209/Copper_Ridge_trail_North_Cascades_National_Park_USA_5K_medium.jpg"
        ]
    
        tasks = []
        for url in url_list:
            # 创建任务,下载图片
            task = asyncio.create_task(download(url))
            tasks.append(task)
    
        await asyncio.wait(tasks)
    
    if __name__ == '__main__':
        lop = asyncio.get_event_loop()
        lop.run_until_complete(main())
    

    总结

    • 线程池
      • 需要反复执行单个函数
    • 多进程 + 线程池
      • 需要执行多个互相不打扰的函数时
      • 需要执行多个相互作用的函数时
    • 协程
      • 存在堵塞(网络请求、文件读写)时
  • 相关阅读:
    Unable to load dbxmss.dll (ErrorCode 16). It may be missing from the system Path
    同一网内机器无法连通解决一例
    Day.24
    Day.24
    Day.23
    Day.22
    Day.23
    Day.21
    Day.22
    Day.01-Day.20
  • 原文地址:https://www.cnblogs.com/CourserLi/p/16543185.html
Copyright © 2020-2023  润新知