day41 - 異步IO、協程

目錄

(見右側目錄欄導航)
python

- 1. 前言
- 2. IO的五種模型
- 3. 協程
    - 3.1 協程的概念
- 4. Gevent 模塊
    - 4.1 gevent 基本使用
    - 4.2 gevent應用一:爬蟲
    - 4.3 gevent應用二:網絡編程

編程

1. 前言

CPU的速度遠遠快於磁盤、網絡等IO。在一個線程中,CPU執行代碼的速度極快,然而,一旦遇到IO操做,如讀寫文件、發送網絡數據時,就須要等待IO操做完成,才能繼續進行下一步操做。這種狀況稱爲同步IO。在IO操做的過程當中,當前線程被掛起,而其餘須要CPU執行的代碼就沒法被當前線程執行了。由於一個IO操做就阻塞了當前線程,致使其餘代碼沒法執行,因此咱們必須使用多線程或者多進程來併發執行代碼,爲多個用戶服務。每一個用戶都會分配一個線程,若是遇到IO致使線程被掛起,其餘用戶的線程不受影響。多線程和多進程的模型雖然解決了併發問題,可是系統不能無上限地增長線程。因爲系統切換線程的開銷也很大,因此,一旦線程數量過多,CPU的時間就花在線程切換上了,真正運行代碼的時間就少了,結果致使性能嚴重降低。因爲咱們要解決的問題是CPU高速執行能力和IO設備的龜速嚴重不匹配,多線程和多進程只是解決這一問題的一種方法,另外一種解決IO問題的方法是異步IO。當代碼須要執行一個耗時的IO操做時,它只發出IO指令,並不等待IO結果,而後就去執行其餘代碼了。一段時間後,當IO返回結果時,再通知CPU進行處理。網絡

 

2. IO 的五種模型

  (1)blocking IO (阻塞IO)多線程

  (2)noblocking IO (非阻塞IO)併發

  (3)IO multiplexing (IO多路複用)異步

  (4)signal driven IO(信號驅動IO) -- 不經常使用socket

  (5)asynchronous IO (異步IO)async

 

在理解上面五種IO模式以前須要理解如下4個概念:ide

  同步、異步、阻塞、非阻塞異步編程

 

2.1 同步和異步

  同步和異步關注的是消息通訊機制

  同步:在發出一個調用時,沒獲得結果以前,該調用就不返回。可是一旦調用返回就獲得返回值(結果)了,調用者須要主動等待這個調用的結果。

  異步:在發送一個調用時,這個調用就直接返回了,無論返回有沒有結果。當一個異步過程調用發出後,被調用者經過狀態,通知調用者,或者經過回調函數處理這個調用

 

2.2 阻塞和非阻塞

  阻塞和非阻塞關注的是程序在等待調用結果時的狀態

  阻塞:調用結果返回以前,當前線程會被掛起。調用線程只有在獲得結果以後才返回;

  非阻塞:在不能當即獲得結果以前,該調用不會掛起當前線程

 

  有一個很好的例子說明這4者之間的關係:

    老張愛喝茶,廢話不說,煮開水。 出場人物:老張,水壺兩把(普通水壺,簡稱水壺;會響的水壺,簡稱響水壺)。
          1 老張把水壺放到火上,立等水開。(同步阻塞) 老張以爲本身有點傻
          2 老張把水壺放到火上,去客廳看電視,時不時去廚房看看水開沒有。(同步非阻塞) 老張仍是以爲本身有點傻,因而變高端了,買了把會響笛的那種水壺。水開以後,能大聲發出嘀~~~~的噪音。
          3 老張把響水壺放到火上,立等水開。(異步阻塞) 老張以爲這樣傻等意義不大
          4 老張把響水壺放到火上,去客廳看電視,水壺響以前再也不去看它了,響了再去拿壺。(異步非阻塞) 老張以爲本身聰明瞭。
        
          所謂同步異步,只是對於水壺而言。 普通水壺,同步;響水壺,異步。 雖然都能幹活,但響水壺能夠在本身完工以後,提示老張水開了。這是普通水壺所不能及的。 同步只能讓調用者去輪詢本身(狀況2中),形成老張效率的低下。
          所謂阻塞非阻塞,僅僅對於老張而言。 立等的老張,阻塞;看電視的老張,非阻塞。 狀況1和狀況3中老張就是阻塞的,媳婦喊他都不知道。雖然3中響水壺是異步的,可對於立等的老張沒有太大的意義。因此通常異步是配合非阻塞使用的,這樣才能發揮異步的效用。

 

3. 協程

3.1 協程的概念

  進程是資源分配的最小單位,線程是CPU調度的基本單位, 在Cpython中,因爲GIL鎖的存在,通常來講,同一時間片只有一個線程在cpu中運行,爲了提升單線程的效率,這裏提出了協程的概念。

  協程:是單線程下的併發,又稱微線程,纖程。英文名Coroutine。一句話說明什麼是協程:協程是一種用戶態的輕量級線程,即協程是由用戶程序本身控制調度的。

  須要強調:

    1. python的線程屬於內核級別的,即由操做系統控制調度(如單線程遇到io或執行時間過長就會被迫交出cpu執行權限,切換其餘線程運行)

    2. 單線程內開啓協程,一旦遇到io,就會從應用程序級別(而非操做系統)控制切換,以此來提高效率(!!!非io操做的切換與效率無關)

 

  對比操做系統控制線程的切換,用戶在單線程內控制協程的切換

  

  優勢以下:

    1. 協程的切換開銷更小,屬於程序級別的切換,操做系統徹底感知不到,於是更加輕量級

    2. 單線程內就能夠實現併發的效果,最大限度地利用cpu

  缺點以下:

    1. 協程的本質是單線程下,沒法利用多核,能夠是一個程序開啓多個進程,每一個進程內開啓多個線程,每一個線程內開啓協程

    2. 協程指的是單個線程,於是一旦協程出現阻塞,將會阻塞整個線程

 

  總結協程的特色:

    1. 必須在只有一個單線程裏實現併發

    2. 修改共享數據不需加鎖

    3. 用戶程序裏本身保存多個控制流的上下文棧

    4. 一個協程遇到IO操做自動切換到其餘協程

 

4. Gevent 模塊

4.1 gevent 基本使用

  Gevent 是一個第三方庫,能夠輕鬆經過gevent實現併發同步或異步編程。

  g1=gevent.spawn(func,1,,2,3,x=4,y=5)
    建立一個協程對象g1,spawn括號內第一個參數是函數名,如eat,後面能夠有多個參數,能夠是位置實參或關鍵字實參,都是傳給函數eat的
  g2=gevent.spawn(func2)
  g1.join()
    等待g1結束
  g2.join()
    等待g2結束
  或者上述兩步合做一步:
  gevent.joinall([g1,g2])
  g1.value
    拿到func1的返回值

 

  使用gevent 遇到IO就切換實例:

import gevent


def eat():
    print('eat start...')
    gevent.sleep(2)
    print('eat end.')


def play():
    print('play start...')
    gevent.sleep(2)
    print('play end.')


if __name__ == '__main__':
    g1 = gevent.spawn(eat)
    g2 = gevent.spawn(play)
    g1.join()
    g2.join()

    print('----主-----')

 

 

  

  上例gevent.sleep(2)模擬的是gevent能夠識別的io阻塞,而time.sleep(1)或其餘的阻塞,gevent是不能直接識別的須要用下面一行代碼,打補丁,就能夠識別了

  from gevent import monkey;monkey.patch_all()必須放到被打補丁者的前面,如time,socket模塊以前或者咱們乾脆記憶成:要用gevent,須要將from gevent import monkey;monkey.patch_all()放到文件的開頭

from gevent import monkey; monkey.patch_all()
import gevent
import time


def eat():
    print('eat start...')
    time.sleep(2)
    print('eat end.')


def play():
    print('play start...')
    time.sleep(2)
    print('play end.')


if __name__ == '__main__':
    g1 = gevent.spawn(eat)
    g2 = gevent.spawn(play)
    g1.join()
    g2.join()

    print('----主-----')

 

 

  咱們能夠用threading.current_thread().getName()來查看每一個g1和g2,查看的結果爲DummyThread-n,即假線程

from gevent import monkey; monkey.patch_all()
import threading
import gevent
import time


def eat():
    print(threading.current_thread().name)
    print('eat start...')
    time.sleep(2)
    print('eat end.')


def play():
    print(threading.current_thread().name)
    print('play start...')
    time.sleep(2)
    print('play end.')


if __name__ == '__main__':
    g1 = gevent.spawn(eat)
    g2 = gevent.spawn(play)
    g1.join()
    g2.join()

    print('----主-----')

執行結果:
DummyThread-1
eat start...
DummyThread-2
play start...
(阻塞2秒)
eat end.
play end.
----主-----

 

 

4.2 gevent 應用一:爬蟲

from gevent import monkey; monkey.patch_all()
import gevent
import requests


def get(url):
    print('GET:', url)
    response = requests.get(url)
    if response.status_code == 200:
        print('%d bytes recevied from %s' % (len(response.text), url))


if __name__ == '__main__':
    gevent.joinall([
        gevent.spawn(get, 'https://www.baidu.com'),
        gevent.spawn(get, 'https://www.taobao.com'),
        gevent.spawn(get, 'https://www.jd.com')])
gevent-爬蟲

 

 

4.3 gevent 應用二:網絡編程

  經過gevent實現單線程下的socket併發
  注意:from gevent import monkey;monkey.patch_all()必定要放到導入socket模塊以前,不然gevent沒法識別socket的阻塞

from gevent import spawn, monkey;monkey.patch_all()
import socket


def server(ip_port):
    sk_server = socket.socket()
    sk_server.bind(ip_port)
    sk_server.listen(5)
    while True:
        conn, addr = sk_server.accept()
        spawn(walk, conn)


def walk(conn):
    conn.send(b'welcome!')
    try:
        while True:
            res = conn.recv(1024)
            print(res)
            conn.send(res.upper())
    except Exception as e:
        print(e)
    finally:
        conn.close()


if __name__ == '__main__':
    server(('localhost', 8080))
server.py
import socket

sk_client = socket.socket()
sk_client.connect(('localhost', 8080))
res = sk_client.recv(1024)
print(res)
while True:
    inp = input('>>>').strip()
    if not inp: continue
    sk_client.send(inp.encode())
    print(sk_client.recv(1024))
client.py
相關文章
相關標籤/搜索