我们在其他编程语言中经常会接触到多进程、多线程的概念和使用,移动客户端一般还是多线程实现任务并发的情况多一点,多进程主要还是在pc或是服务器上涉及的比较多。
多进程、多线程目的都是为了实现任务的并发运行以达到更高的效率。其都是基于操作系统的机制来实现的,但python中的协程却不是,其完全是一种程序层面的实现机制。他们之间的关系是多进程 > 多线程 > 多协程
python中的协程主要是针对高并发的I/O操作,比如网络I/O、文件读写IO,并不适用于CPU密集型的任务。可在单个线程中开启多个协程任务来并发处理多个IO操作,协程没有切换线程的系统开销,也没有多个线程之间对共享资源的加锁抢占机制,因此执行效率非常高。
可以这么理解,线程中的多个协程任务其实并没有做到真正的并发,也就是说同一个时刻里只有一个任务在执行:
这里所说的时刻要细分到非常小粒度,因为计算机的执行速度是非常快的,我们通常所认为的1秒或是零点几秒的时间内,计算机其实已经做了很多个任务了,只不过这段时间里,这些任务执行还是有先后顺序的;所以在我们人为看来好像就实现了在同一时间内并发执行了多个任务。多线程的并发其实在单核cpu里也是这么一个道理,并不是真正的并发
协程是一种程序层面的控制流程,通过在程序流程中实现一个事件循环机制runloop,来不断的遍历循环中是否有等待执行的任务,当发现有则取出该任务执行,当该任务遇到网络请求IO、文件读写IO时,程序主动挂起当前任务,转而在循环中寻找下一个任务,当下一个任务也遇到IO操作时,同样挂起它,转而再去检查之前挂起的任务IO操作是否完成,如果完成,继续该任务,如果仍未完成,则再去循环中寻找另外等待的任务,如此循环。
因此,协程就是线程中某一时刻所执行的子任务,他共用该线程的各种资源,由于上述解释的不是真并发,所以也就没有资源加锁抢占。如果没有协程,线程在串行执行任务时,如果遇到耗时IO操作,便只能一直等待,线程里后续的其他任务也就只能等待,在JavaScript中,其实也实现了这种机制Promise,和python的协程是一样的,因为JavaScript本身就是单线程的,为了能达到并发的效果,这种协程机制就有用武之地了。
简述python中协程编码实现
由于python中的协程实现历史久远,主要说下python3.6中的实现,因为自3.6版本开始,python算是比较稳定的内置支持协程了,之前的版本要么依赖第三方实现,如gevent库,要么本身的语法不是很友好,如python3.4中,由@asyncio.cortinue等这样的装饰器来实现;3.6版本开始支持async/await这样的语法糖。
Python的协程本质是基于python的生成器实现
例子:获取各个代理网站提供的免费代理。
入口脚本getter.py:
from proxypool.crawlers import __all__ as crawlers_cls #crawlers_cls为包proxypool.crawlers目录下编写的各个代理网站脚本文件
import asyncio
loop = asyncio.get_event_loop()
def run():
tasks = [get_crawler(crawler) for crawler in crawlers_cls]
loop.run_until_complete(asyncio.wait(tasks))
async def get_crawler(crawler): #协程函数
print(f'crawler {crawler} to get proxy')
async for proxy in crawler.crawl(): #crawler.crawl()为一个异步生成器,可用async for迭代
print(f'crawler {crawler} -- 得到代理:{proxy}')
print(f'crawler {crawler} to get proxy---end!!!')
run()
代理爬虫脚本基类base.py:
from loguru import logger
from proxypool.setting import GET_TIMEOUT
import aiohttp
import asyncio
import traceback
class BaseCrawler(object):
urls = []
def __init__(self):
self.loop = asyncio.get_event_loop()
@logger.catch
async def fetch(self, url, **kwargs):
retryTimes = 10 #重试次数
flag = False #单个网络请求的结果,状态码为200时返回True
html = None
while retryTimes > 0 and not flag:
retryTimes -= 1
print(f'fetching {url}')
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
try:
async with session.get(url, **kwargs) as response:
print(response.status)
if response.status == 200:
flag = True
html = await response.text()
elif response.status == 403:
flag = True
raise RuntimeError('403 Forbbiden') #主动抛出403异常,403说明对方做了反扒处理,继续重试已无意义,所以flag置为True
else:
await asyncio.sleep(0.5)
except:
#上述try中抛出的异常会在此捕获,不会再传到上一级函数中,所以只会影响单个网络请求的结果,并列的其他网络请求不会受影响
print(traceback.format_exc())
await asyncio.sleep(0.5)
return html
@logger.catch
async def crawl(self):
"""
crawl main method
"""
for url in self.urls:
html = await self.fetch(url)
if html == None:
continue
for proxy in self.parse(html): #self.parse(html)是一个生成器,含关键字yield
print(f'fetched proxy {proxy.string()} from {url}')
yield proxy
代理爬虫脚本daili66.py:
from pyquery import PyQuery as pq
from proxypool.schemas.proxy import Proxy
from proxypool.crawlers.base import BaseCrawler
import asyncio
BASE_URL = 'http://www.66ip.cn/{page}.html'
MAX_PAGE = 7
class Daili66Crawler(BaseCrawler):
"""
daili66 crawler, http://www.66ip.cn/1.html
"""
urls = [BASE_URL.format(page=page) for page in range(3, MAX_PAGE + 1)]
def parse(self, html): #各个代理网站具体的解析规则方法
"""
parse html file to get proxies
:return:
"""
doc = pq(html)
trs = doc('.containerbox table tr:gt(0)').items() #pyquery 的items()方法时一个生成器,不是直接返回一个列表
for tr in trs:
host = tr.find('td:nth-child(1)').text()
port = int(tr.find('td:nth-child(2)').text())
yield Proxy(host=host, port=port)