大家好,我是剑南。
今天为大家带来的内容是协程概念的详解以及配套实战内容。
协程
协程的概念
协程,又称作是微线程,协程是一种用户态的轻量级编程。
其实,怎么样来理解会更好一些呢?
我这样说吧,线程是系统级别的,它们由操作系统调度;协程是程序级别的,由程序员根据自己的需求去调度。我们把线程中一个个函数叫做子程序,那么子程序在执行过程中可以中断,然后去执行别的子程序;别的子程序也可以中断回来继续执行之前的子程序,这就是协程。也就是说,在同一个线程下的一段代码,在执行的过程中,有可能会发生中断,然后跳去执行另一段代码,当再次回来执行之前的代码块的时候,就从之前中断的地方开始执行。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切换回来的时候,恢复之前保存的寄存器上下文和栈。因此:协程可以保留上一次调用时的状态,每次过程重入的时候,就相当于进入上一次调用的状态。换种说法就是进入上一次离开时所处逻辑留的位置。
协程的优点
-
协程不需要像线程一样存在上下文切换的开销,避免了无意义的调度,由此可以提高性能。 -
无需原子操作锁定及同步的开销 -
方便切换控制流,简单化编程模型 -
高并发+高拓展性+低成本:一个CPU支持上万的协程都不是问题,所有很合适用于高并发处理。
协程的缺点
-
无法利用多核资源:协程的本质是单线程,因此它不能将单个CPU的多个核都用上,协程需要和进程配合才能运行在多核CPU上。 -
进行阻塞操作时会阻塞掉整个程序
python如何实现协程
协程是通过async/await语法进行声明,是编写asyncio应用的推荐方式。具体代码,如下所示:
import asyncio
async def main():
print('hello')
await asyncio.sleep(5)
print('world')
asyncio.run(main())
关键字await有两个作用,一是作为求值的关键字,二是将异步操作变为同步操作;如果方法中使用了await,那么在方法的前面就必须加上async。
一般来说,正常的函数在执行的时候是不会被中断的,所有想要写一个可以中断的函数,就需要添加async。
像上面的代码,await asyncio.sleep(5)
,就是说这个程序被挂起5秒,5秒后再回来执行程序。
await用来声明程序挂起,比如异步程序执行到某一步时需要等待很长的时间,就将挂起,去执行其他异步程序,当挂起条件消失之后,不管当前程序是否执行完毕,都必须退出,去执行之前的程序。
asyncio.run()函数用来运行最高层级的入口点“main()”函数。
等待协程
import asyncio
import time
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
print(f'start at {time.strftime("%X")}')
await say_after(2, 'hello')
await say_after(2, 'world')
print(f'finish at {time.strftime("%X")}')
asyncio.run(main())
运行结果,如下所示:
start at 13:59:21
hello
world
finish at 13:59:25
多任务协程
使用create_task()函数用来并发运行作为asyncio任务的多个协程。如何并发运行两个任务呢?
具体代码,如下所示:
import asyncio
import time
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
task1 = asyncio.create_task(say_after(2, 'hello'))
task2 = asyncio.create_task(say_after(2, 'world'))
print(f'start at {time.strftime("%X")}')
await task1
await task2
print(f'finish at {time.strftime("%X")}')
asyncio.run(main())
运行结果,如下所示:
start at 16:28:53
hello
world
finish at 16:28:55
预期的显示输出时间比之前快了2秒。
可等待对象
如果一个对象可以在await语句中使用,那么它就是可等待对象,可等待对象一共有三种类型:协程、任务与Future。
协程属于python的可等待对象,因此可以在其他协程中被等待,具体代码,如下所示:
import asyncio
async def nested():
return 42
async def main():
nested() # 报错
print(await nested()) # 正常
asyncio.run(main())
对于上面的代码,有一点要注意,那就是当我们创建了一个协程方法时,需要调用,就一定要用关键字await。
协程函数指的是:定义形式为async def的函数。
协程对象指的是:调用协程函数所调用的对象。
任务用来并行的调度协程,当一个协程通过create_task()等函数封装为一个任务,该协程会被自动调度执行。
具体代码,如下所示:
import asyncio
async def nested():
return 42
async def main():
task = asyncio.create_task(nested())
await task
asyncio.run(main())
而Future是一种低层级的对象,表示一个异步操作的最终结果。当一个Future对象被等待,这意味着协程将保持等待直到该Future对象在其他地方操作完毕。通常情况下没有必要在应用级的代码中创建Future对象。
并发运行任务
具体代码,如下所示:
import asyncio
async def factorial(name, number):
f = 1
for i in range(1, number+1):
print(f'Task {name}: Compute factorial({number}), currrently i = {i}...')
await asyncio.sleep(1)
f *= i
print(f'Task {name}: factorial({number}) = {f}')
return f
async def main():
L = await asyncio.gather(factorial('A', 2), factorial('B', 3), factorial('C', 4))
print(L)
asyncio.run(main())
运行结果,如下所示:
Task A: Compute factorial(2), currrently i = 1...Task B: Compute factorial(3), currrently i = 1...Task C: Compute factorial(4), currrently i = 1...Task A: Compute factorial(2), currrently i = 2...Task B: Compute factorial(3), currrently i = 2...Task C: Compute factorial(4), currrently i = 2...Task A: factorial(2) = 2Task B: Compute factorial(3), currrently i = 3...Task C: Compute factorial(4), currrently i = 3...Task B: factorial(3) = 6Task C: Compute factorial(4), currrently i = 4...Task C: factorial(4) = 24[2, 6, 24]
通过gather()方法,可以将一个协程对象作为任务调度,当所有的可等待对象都已经完成,结果将是由一个所有值聚合而成的列表。
实战-彼岸图网
如上图所示,本次的实战案例是爬取彼岸图网,网址如下:
https://pic.netbian.com/4kdongman/
本次实战,需要各位小伙伴们掌握xpath语法与正则表达式。
在本次项目中,最重要的就是找到4K图片的下载地址,这一点很重要。
当我点击下载原图时,我便捕获到了真正的url地址,如下所示:
https://pic.netbian.com/downpic.php?id=27068&classid=66
这里有两个查询参数,一个是id,另一个是classid,对于这两个参数,我的第一想法是看看网页中是否存在这两个参数的值。
从上图可以看到,在网页上存在这两个参数的值,那么只需要利用xpath与正则表达式即可。
创建协程函数
在这里,我主要有创建三个任务,一个是获取详情页的url,一个是获取图片的url,最后一个保存图片。
具体代码,如下所示:
async def get_url(url):
proxies = {
'http://': 'http://182.38.188.206:4210',
'https://': 'http://182.38.188.206:4210'
}
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36'
}
response = httpx.get(url, headers=headers, proxies=proxies)
html = etree.HTML(response.text)
hrefs = html.xpath('//ul[@class="clearfix"]/li/a/@href')
hrefs = ['https://pic.netbian.com/' + href for href in hrefs]
return hrefs
async def get_img_url(hrefs):
args = []
img_urls = []
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36'
}
proxies = {
'http://': 'http://182.38.188.206:4210',
'https://': 'http://182.38.188.206:4210'
}
for href in hrefs:
response = httpx.get(href, headers=headers, proxies=proxies)
html = etree.HTML(response.text)
src = html.xpath('//script[last()]/@src')[0]
arg = re.findall('.*?(classid=d+&id=d+).*?', src)[0]
img_url = 'https://pic.netbian.com/downpic.php?' + arg
img_urls.append(img_url)
return img_urls
async def save_img(img_urls):
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36'
}
proxies = {
'http://': 'http://182.38.188.206:4210',
'https://': 'http://182.38.188.206:4210'
}
for img_url in img_urls:
i = 1
response = httpx.get(img_url, headers=headers, proxies=proxies)
data = response.content
with open(f'./picture/{i}.jpg', 'wb') as f:
f.write(data)
i += 1
创建任务
async def main():
url = 'https://pic.netbian.com/4kdongman/'
task1 = asyncio.create_task(get_url(url))
hrefs = await task1
task2 = asyncio.create_task(get_img_url(hrefs))
img_urls = await task2
print(img_urls)
task3 = asyncio.create_task(save_img(img_urls))
await task3
最后
本篇文章主要是想要大家掌握到协程的创建与使用,后期关于协程的更多用法,我会在遇到其他实例的时候再和大家做分享。
文章的每一个字都是我用心敲出来的,点个【赞】与【在看】,让我知道你和我一样都在努力学习。
原文始发于微信公众号(小志Codings):python 协程详解
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/236596.html