Python之协程的理解

我们在其他编程语言中经常会接触到多进程、多线程的概念和使用,移动客户端一般还是多线程实现任务并发的情况多一点,多进程主要还是在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)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容