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()
多进程 + 线程池
- 当需要执行多个互相不打扰的函数时,这是个很好的模型,因为一个进程中可以跑多个线程
#! /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 秒
- 当需要执行多个相互作用的函数时,这也是很好的模型,因为一个主进程中有多个子进程,子进程可以在主线程中相互作用
#! /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 函数也可以单独分离出一个函数,用线程池运行
协程
- 协程的标准写法
#! /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())
"""
- 协程在爬虫中的运用(以下载图片为例)
#! /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())
总结
- 线程池
- 需要反复执行单个函数
- 多进程 + 线程池
- 需要执行多个互相不打扰的函数时
- 需要执行多个相互作用的函数时
- 协程
- 存在堵塞(网络请求、文件读写)时