快樂的 asyncio 學(xué)習(xí)

其實(shí) asyncio 的學(xué)習(xí)一點(diǎn)也不快樂

一、python 的多線程和多進(jìn)程

要想理解 asyncio 的異步編程,需要簡(jiǎn)單了解一下 python 的多線程和多進(jìn)程知識(shí)

  • 1岖常、多線程

pythonGIL 機(jī)制,因此,python 的多線程雖然是操作系統(tǒng)的原生線程纵散,但無法完成真正的并行運(yùn)行,而僅僅在線程處于睡眠或者等待 I/O 時(shí)隐圾,才會(huì)發(fā)揮真正的多線程作用伍掀。

  • 1.1、睡眠
    • time.sleep()
    • threading.Lock
    • 線程模塊其他同步對(duì)象
  • 1.2暇藏、I/O
    • aiohttp
    • open
  • 1.3蜜笤、釋放 GIL
    • 所謂釋放 GIL 是指當(dāng)前線程執(zhí)行一定長(zhǎng)度字節(jié)碼或者一段時(shí)間后,釋放 GIL盐碱,由系統(tǒng)將 GIL 分配給其他線程把兔,當(dāng)前線程進(jìn)入等待狀態(tài)
    • py2 解釋器每執(zhí)行 1000 字節(jié)碼釋放 GIL
    • py3 解釋器每執(zhí)行 15ms 釋放 GIL
  • 1.4、GIL 全局解釋器鎖
    • 同一進(jìn)程同一時(shí)間只有一個(gè)線程在執(zhí)行字節(jié)碼瓮顽,但睡眠線程或者 I/O 操作相關(guān)線程不受 GIL 鎖限制县好,允許并發(fā)執(zhí)行。(GIL 保證同一時(shí)刻只有一個(gè)線程對(duì)共享資源進(jìn)行存取暖混,省去線程間資源鎖的開銷)
  • 1.5缕贡、GIL 原理
      /* s.connect((host, port)) method */
      static PyObject *
      sock_connect(PySocketSockObject *s, PyObject *addro)
      {
          sock_addr_t addrbuf;
          int addrlen;
          int res;
       
          /* convert (host, port) tuple to C address */
          getsockaddrarg(s, addro, SAS2SA(&addrbuf), &addrlen);
       
          Py_BEGIN_ALLOW_THREADS
          res = connect(s->sock_fd, addr, addrlen);
          Py_END_ALLOW_THREADS
       
          /* error handling and so on .... */
      }
    
    • Py_BEGIN_ALLOW_THREADS 放棄 GIL
    • Py_END_ALLOW_THREADS 重新獲取 GIL,一個(gè)線程會(huì)在這個(gè)位置阻塞儒恋,等待另一個(gè)線程釋放鎖善绎;一旦出現(xiàn)這個(gè)情況,等待的線程會(huì)搶奪回鎖诫尽,并恢復(fù)字節(jié)碼的執(zhí)行
    • 簡(jiǎn)而言之:允許有N個(gè)線程在網(wǎng)絡(luò) I/O 堵塞禀酱,或等待重新獲取 GIL,但只有一個(gè)線程運(yùn)行字節(jié)碼
  • 1.6牧嫉、示例
    • 睡眠阻塞
      import time
      from threading import Thread
      from datetime import datetime
      
      def write(i):
          print('{} start write --> {}'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), i))
          time.sleep(4)
          print('{} end write --> {}'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), i))
      
      def fun():
          print('start ...')
          for i in range(3):
              Thread(target=write, args=(i,), daemon=False).start()
          print('end ...')
      # 輸出結(jié)果
      start ...
      2018-02-09 23:58:25 start write --> 0
      2018-02-09 23:58:25 start write --> 1
      2018-02-09 23:58:25 start write --> 2
      end ...
      2018-02-09 23:58:29 end write --> 0
      2018-02-09 23:58:29 end write --> 1
      2018-02-09 23:58:29 end write --> 2
    
    • CPU 阻塞
      import time
      from threading import Thread
      from datetime import datetime
      
      def write(n):
          print('{} start write --> {}'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), n))
          l, sum_ = list(range(100000000)), 0
          for i in l:
              sum_ += i
          print('{} end write --> {}'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), n))
      
      def fun():
          print('start ...')
          for i in range(3):
              Thread(target=write, args=(i,), daemon=False).start()
          print('end ...')
      # 輸出結(jié)果
      start ...
      2018-02-10 00:13:55 start write --> 0
      2018-02-10 00:13:58 start write --> 1
      2018-02-10 00:14:02 start write --> 2
      end ...
      2018-02-10 00:14:27 end write --> 0
      2018-02-10 00:14:32 end write --> 1
      2018-02-10 00:14:35 end write --> 2
    
    • 總結(jié)
      • 對(duì)于睡眠操作或者 I/O 操作剂跟,多線程的作用非常明顯,明顯減少所消耗總時(shí)間酣藻;
      • 對(duì)于 CPU 計(jì)算型操作曹洽,多線程操作反而因?yàn)槎嗑€程間獲取 GIL 而增加總的消耗時(shí)間。
  • 2辽剧、python 多進(jìn)程

python 多進(jìn)程即其他語言中的多進(jìn)程概念送淆,不再累述

二、異步編程思想
  • 1怕轿、協(xié)程 coroutine
  • 2偷崩、任務(wù) Task
  • 3辟拷、事件循環(huán) loop
1、Task 對(duì)象主要包含 協(xié)程(coro)和輪詢對(duì)象(loop)2個(gè)屬性阐斜;
2衫冻、Loop 對(duì)象使用隊(duì)列和堆數(shù)據(jù)結(jié)構(gòu)存放Handle對(duì)象(綁定了回調(diào)函數(shù),如:task的_step方法等)谒出。隊(duì)列中存放的是可以立即執(zhí)行的任務(wù)隅俘,堆中存放的是一定時(shí)間后要執(zhí)行的任務(wù)。對(duì)于yield from asyncio.sleep() 的任務(wù)則是添加到堆中笤喳,到達(dá)指定時(shí)間后執(zhí)行为居。

# 簡(jiǎn)單的調(diào)用示例
import asyncio

@asyncio.coroutine
def coro_fun():
    yield from range(10)

loop = asyncio.get_event_loop()

loop.run_until_complete(coro_fun()) 
# or
tasks = [asyncio.ensure_future(coro_fun())]
loop.run_until_complete(asyncio.wait(tasks))
三、什么是協(xié)程

進(jìn)程或線程間的創(chuàng)建依賴于系統(tǒng)底層進(jìn)程或線程庫莉测,其運(yùn)行也依賴于系統(tǒng)的任務(wù)調(diào)度系統(tǒng)颜骤,在任務(wù)切換時(shí),cpu 需要進(jìn)行上下文切換捣卤。
協(xié)程是運(yùn)行在單線程上忍抽,協(xié)程間的切換是在語言層級(jí)實(shí)現(xiàn)的,依賴于對(duì)應(yīng)協(xié)程庫董朝。

  • 在單線程執(zhí)行過程中鸠项,如果涉及 sleep,網(wǎng)絡(luò) IO 操作時(shí)子姜,線程會(huì)阻塞住等待任務(wù)完成祟绊;
  • 但如果使用協(xié)程,輪詢對(duì)象(loop)在輪詢事件時(shí)哥捕,會(huì)分別處理就緒對(duì)象 _ready 和調(diào)度對(duì)象 _scheduled 以及 select 監(jiān)聽對(duì)象牧抽。每次進(jìn)行輪詢時(shí),會(huì)篩選出調(diào)度對(duì)象中滿足執(zhí)行條件的對(duì)象以及 select 監(jiān)聽到可讀或可寫的對(duì)象遥赚,添加到就緒對(duì)象中扬舒,由 loop 對(duì)象進(jìn)行循環(huán)調(diào)度。
    • _ready
    • _ready += 滿足執(zhí)行條件的 _scheduled 對(duì)象
    • _ready += select 監(jiān)聽到的可讀或可寫對(duì)象
    • loop 遍歷執(zhí)行 _ready 中的對(duì)象
四凫佛、什么是期物

期物對(duì)象的設(shè)計(jì)初衷是讲坎,期物用來追蹤任務(wù)或者協(xié)程的運(yùn)行狀態(tài)。一般使用中愧薛,期物用來追蹤 _ready, _scheduled, select 監(jiān)聽的對(duì)象晨炕,在各對(duì)象執(zhí)行完成后設(shè)置期物對(duì)象狀態(tài)為 FINISHED,并將設(shè)置 _loop 輪詢對(duì)象狀態(tài)為 close 的函數(shù)注冊(cè)到 loop 對(duì)象的 _ready 隊(duì)列中毫炉,由 loop 對(duì)象輪詢完成瓮栗。

五、源代碼分析
  • 關(guān)于 _ code _.co_flags
    # 每個(gè)函數(shù)或方法都有 __code__ 魔法方法 以及其對(duì)應(yīng)的 co_flags 值
    # 在 Cpython 中,
    1费奸、生成器函數(shù)的標(biāo)識(shí)符為 CO_GENERATOR 即 0x20鲸郊,
    2、協(xié)程函數(shù)的標(biāo)識(shí)符為 CO_COROUTINE 即 0x180
    3货邓、CO_ITERABLE_COROUTINE 即 0x100
    
    # 通過對(duì)函數(shù)對(duì)象的 __code__.co_flags 與 對(duì)應(yīng)的標(biāo)識(shí)符做位與運(yùn)算,如果是真值四濒,則表明函數(shù)對(duì)象屬于生成器函數(shù)或協(xié)程函數(shù)
    
    def gen_fun():
        yield from range(10)
    >>> gen_fun.__code__.co_flags  # 99
    >>> 99 & 0x20  # 32, True
    >>> 99 & 0x180  # 0, False
    
    async def asy_fun():
        await sleep(4)
    >>> asy_fun.__code__.co_flags  # 227
    >>> 99 & 0x20  # 32, True
    >>> 227 & 0x180  # 128, True
    
  • 關(guān)于類型判斷
    from collections import Iterator, Awaitable
    # 判斷迭代器 和 Awaitable 對(duì)象
    class A:
        def __iter__(self):
            return iter([1,2,3,4,5])
        def __await__(self):
            return iter([1,2,3,4,5])
    a = A()
    >>> isinstance(a, Iterator)  # True
    >>> isinstance(a, Awaitable)  # True
    
    # 判斷是否為協(xié)程等
    import inspect
    async def asy_fun():
        await a
    >>> inspect.iscoroutine(asy_fun())  # True
    
  • @asyncio.coroutine
    def coroutine(func):
        # 將一個(gè)生成器標(biāo)記為協(xié)程换况,如果在destroyed前沒有調(diào)用,則會(huì)記錄錯(cuò)誤
    
        # 這個(gè)方法是使用 inspect.iscoroutinefunction 方法判斷是否為協(xié)程方法盗蟆,使用 types.coroutine 裝飾的生成器戈二,或 async def 語法定義的函數(shù)都會(huì)返回 True
        if _inspect_iscoroutinefunction(func):
            return func
    
        # 使用 co_flags 判斷是否為生成器
        if inspect.isgeneratorfunction(func):
            coro = func
        else:
            @functools.wraps(func)
            def coro(*args, **kw):
                res = func(*args, **kw)
                
                # 判斷 res 是否為期物,生成器 或 協(xié)程包裝類 實(shí)例
                if isinstance(res, futures.Future) or inspect.isgenerator(res) or \
                        isinstance(res, CoroWrapper):
                    res = yield from res
    
                elif _AwaitableABC is not None:
                    # py 3.5 才會(huì)有 Awaitable 類
                    try:
                        # 如果有 __await__屬性喳资,__await__屬性只會(huì)返回一個(gè)不是協(xié)程的迭代器
                        await_meth = res.__await__
                    except AttributeError:
                        pass
                    else:
                        # 如果是 Awaitable 對(duì)象
                        if isinstance(res, _AwaitableABC):
                            # 使用 yield from 處理其迭代器
                            res = yield from await_meth()
                return res
    
        # 使用 types.coroutine 包裝 coro(注意觉吭,多層 @types.coroutine 裝飾不會(huì)影響,會(huì)直接return裝飾的值)
        if not _DEBUG:
            if _types_coroutine is None:
                wrapper = coro
            else:
                wrapper = _types_coroutine(coro)
        else:
            @functools.wraps(func)
            def wrapper(*args, **kwds):
              
                # 使用協(xié)程包裝器處理
                w = CoroWrapper(coro(*args, **kwds), func=func)
                if w._source_traceback:
                    del w._source_traceback[-1]
                # 如果是 py 3.5 則包裝增加 協(xié)程 對(duì)象的屬性,否則包裝為 生成器 對(duì)象的屬性
                w.__name__ = getattr(func, '__name__', None)
                w.__qualname__ = getattr(func, '__qualname__', None)
                return w
        
        # 用以別處使用 asyncio.iscoroutinefunction() 判斷為 True 的作用
        wrapper._is_coroutine = True  # For iscoroutinefunction().
        return wrapper
    
  • @types.coroutine
    def coroutine(func):
      # 將一個(gè)普通的生成器函數(shù)轉(zhuǎn)化為協(xié)程
    
      if not callable(func):
          raise TypeError('types.coroutine() expects a callable')
    
      if (func.__class__ is FunctionType and
          getattr(func, '__code__', None).__class__ is CodeType):
    
          # 獲取函數(shù)的 co_flags
          co_flags = func.__code__.co_flags
    
          # 檢查是否為協(xié)程函數(shù)
          if co_flags & 0x180:
              return func
    
          # 檢查是否為生成器函數(shù)仆邓,此步主要作用是將生成器的 co_flags 同 0x100 做位或運(yùn)算鲜滩,將其標(biāo)識(shí)變更為協(xié)程標(biāo)識(shí)
          if co_flags & 0x20:
              # TODO: Implement this in C.
              co = func.__code__
              func.__code__ = CodeType(
                  co.co_argcount, co.co_kwonlyargcount, co.co_nlocals,
                  co.co_stacksize,
                  co.co_flags | 0x100,  # 0x100 == CO_ITERABLE_COROUTINE
                  co.co_code,
                  co.co_consts, co.co_names, co.co_varnames, co.co_filename,
                  co.co_name, co.co_firstlineno, co.co_lnotab, co.co_freevars,
                  co.co_cellvars)
              return func
    
      # 用以支持類似生成器的對(duì)象
    
      @_functools.wraps(func)
      def wrapped(*args, **kwargs):
          coro = func(*args, **kwargs)
    
          # 協(xié)程或 co_flags 大于 256 的生成器對(duì)象,直接返回
          if (coro.__class__ is CoroutineType or
              coro.__class__ is GeneratorType and coro.gi_code.co_flags & 0x100):
              return coro
          if (isinstance(coro, _collections_abc.Generator) and
              not isinstance(coro, _collections_abc.Coroutine)):
              # 實(shí)現(xiàn)了生成器抽象類的方法节值,使用生成器包裝器處理成生成器
              return _GeneratorWrapper(coro)
          # 協(xié)程抽象類實(shí)例或其他對(duì)象
          return coro
    
      return wrapped
    
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末徙硅,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子搞疗,更是在濱河造成了極大的恐慌嗓蘑,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件匿乃,死亡現(xiàn)場(chǎng)離奇詭異桩皿,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)幢炸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門泄隔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人阳懂,你說我怎么就攤上這事梅尤。” “怎么了岩调?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵巷燥,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我号枕,道長(zhǎng)缰揪,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮钝腺,結(jié)果婚禮上抛姑,老公的妹妹穿的比我還像新娘。我一直安慰自己艳狐,他們只是感情好定硝,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著毫目,像睡著了一般蔬啡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上镀虐,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天箱蟆,我揣著相機(jī)與錄音,去河邊找鬼刮便。 笑死空猜,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的恨旱。 我是一名探鬼主播辈毯,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼窖杀!你這毒婦竟也來了漓摩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤入客,失蹤者是張志新(化名)和其女友劉穎管毙,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體桌硫,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡夭咬,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了铆隘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卓舵。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖膀钠,靈堂內(nèi)的尸體忽然破棺而出掏湾,到底是詐尸還是另有隱情,我是刑警寧澤肿嘲,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布融击,位于F島的核電站,受9級(jí)特大地震影響雳窟,放射性物質(zhì)發(fā)生泄漏尊浪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望拇涤。 院中可真熱鬧捣作,春花似錦、人聲如沸鹅士。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽掉盅。三九已至嘱朽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間怔接,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工稀轨, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留扼脐,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓奋刽,卻偏偏與公主長(zhǎng)得像瓦侮,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子佣谐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容