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())

总结

  • 线程池
    • 需要反复执行单个函数
  • 多进程 + 线程池
    • 需要执行多个互相不打扰的函数时
    • 需要执行多个相互作用的函数时
  • 协程
    • 存在堵塞(网络请求、文件读写)时
posted @ 2022-09-14 15:59  筱团  阅读(171)  评论(0编辑  收藏  举报