1. 程式人生 > >python網絡編程--協程

python網絡編程--協程

上下文 用戶 返回 回顧 spa png true 分配 修改

1.協程 

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

需要強調的是:

  1. python的線程屬於內核級別的,即由操作系統控制調度(如單線程遇到io或執行時間過長就會被迫交出cpu執行權限,切換其他線程運行)
  2. 單線程內開啟協程,一旦遇到io,就會從應用程序級別(而非操作系統)控制切換,以此來提升效率(!!!非io操作的切換與效率無關)
    對比操作系統控制線程的切換,用戶在單線程內控制協程的切換

優點如下:

  1. 協程的切換開銷更小,屬於程序級別的切換,操作系統完全感知不到,因而更加輕量級

  2. 單線程內就可以實現並發的效果,最大限度地利用cpu

缺點如下:

  1. 協程的本質是單線程下,無法利用多核,可以是一個程序開啟多個進程,每個進程內開啟多個線程,每個線程內開啟協程
  2. 協程指的是單個線程,因而一旦協程出現阻塞,將會阻塞整個線程

總結協程特點:


  必須在只有一個單線程裏實現並發
  修改共享數據不需加鎖
  用戶程序裏自己保存多個控制流的上下文棧

附加:一個協程遇到IO操作自動切換到其它協程(如何實現檢測IO,yield、greenlet都無法實現,就用到了gevent模塊(select機制))

   基於單線程來實現並發,即只用一個主線程(很明顯可利用的cpu只有一個)情況下實現並發,為此我們需要先回顧下並發的本質:切換+保存狀態

   cpu正在運行一個任務,會在兩種情況下切走去執行其他的任務(切換由操作系統強制控制),一種情況是該任務發生了阻塞,另外一種情況是該任務計算的時間過長或有一個優先級更高的程序替代了它

   協程本質上就是一個線程,以前線程任務的切換是由操作系統控制的,遇到I/O自動切換,現在我們用協程的目的就是較少操作系統切換的開銷(開關線程,創建寄存器、堆棧等,在他們之間進行切換等),在我們自己的程序裏面來控制任務的切換。

技術分享圖片

  在介紹進程理論時,提及進程的三種執行狀態,而線程才是執行單位,所以也可以將上圖理解為線程的三種狀態

  其中第二種情況並不能提升效率,只是為了讓cpu能夠雨露均沾,實現看起來所有任務都被“同時”執行的效果,如果多個任務都是純計算的,這種切換反而會降低效率。為此我們可以基於yield來驗證。yield本身就是一種在單線程下可以保存任務運行狀態的方法,我們來簡單復習一下:

  1 yiled可以保存狀態,yield的狀態保存與操作系統的保存線程狀態很像,但是yield是代碼級別控制的,更輕量級
  2 send可以把一個函數的結果傳給另外一個函數,以此實現單線程內程序之間的切換

單純的切換反而會影響效率

#基於yield並發執行,多任務之間來回切換,這就是個簡單的協程的體現,但是他能夠節省I/O時間嗎?不能
import time
def consumer():
    ‘‘‘任務1:接收數據,處理數據‘‘‘
    while True:
        x=yield
        # time.sleep(1) #發現什麽?只是進行了切換,但是並沒有節省I/O時間
        print(‘處理了數據:‘,x)
def producer():
    ‘‘‘任務2:生產數據‘‘‘
    g=consumer()
    next(g)  #找到了consumer函數的yield位置
    for i in range(3):
    # for i in range(10000000):
        g.send(i)  #給yield傳值,然後再循環給下一個yield傳值,並且多了切換的程序,比直接串行執行還多了一些步驟,導致執行效率反而更低了。
        print(‘發送了數據:‘,i)
start=time.time()
#基於yield保存狀態,實現兩個任務直接來回切換,即並發的效果
#PS:如果每個任務中都加上打印,那麽明顯地看到兩個任務的打印是你一次我一次,即並發執行的.
producer() #我在當前線程中只執行了這個函數,但是通過這個函數裏面的send切換了另外一個任務
stop=time.time()

# 串行執行的方式
# res=producer()
# consumer(res) 
# stop=time.time()

print(stop-start)

二:第一種情況的切換。在任務一遇到io情況下,切到任務二去執行,這樣就可以利用任務一阻塞的時間完成任務二的計算,效率的提升就在於此。

import time
def func1():
    while True:
        print(‘func1‘)
        yield

def func2():
    g=func1()
    for i in range(10000000):
        i+1
        next(g)
        time.sleep(3)
        print(‘func2‘)
start=time.time()
func2()
stop=time.time()
print(stop-start)
  協程就是告訴Cpython解釋器,你不是nb嗎,不是搞了個GIL鎖嗎,那好,我就自己搞成一個線程讓你去執行,省去你切換線程的時間,我自己切換比你切換要快很多,避免了很多的開銷,對於單線程下,我們不可避免程序中出現io操作,但如果我們能在自己的程序中(即用戶程序級別,而非操作系統級別)控制單線程下的多個任務能在一個任務遇到io阻塞時就切換到另外一個任務去計算,這樣就保證了該線程能夠最大限度地處於就緒態,即隨時都可以被cpu執行的狀態,相當於我們在用戶程序級別將自己的io操作最大限度地隱藏起來,從而可以迷惑操作系統,讓其看到:該線程好像是一直在計算,io比較少,從而更多的將cpu的執行權限分配給我們的線程。

  協程的本質就是在單線程下,由用戶自己控制一個任務遇到io阻塞了就切換另外一個任務去執行,以此來提升效率。為了實現它,我們需要找尋一種可以同時滿足以下條件的解決方案:

    1. 可以控制多個任務之間的切換,切換之前將任務的狀態保存下來,以便重新運行時,可以基於暫停的位置繼續執行。
    2. 作為1的補充:可以檢測io操作,在遇到io操作的情況下才發生切換

生成器實現協程效果(yield)

import time

def f1():
    for i in range(10):
        time.sleep(0.5)
        print(‘f1>>‘,i)
        yield

def f2():
    g = f1()
    for i in range(10):
        time.sleep(0.5)
        print(‘f2>>‘, i)
        next(g)

f1()
f2()

 

2.greenlet模塊

    如果我們在單個線程內有20個任務,要想實現在多個任務之間切換,使用yield生成器的方式過於麻煩(需要先得到初始化一次的生成器,然後再調用send。。。非常麻煩),而使用greenlet模塊可以非常簡單地實現這20個任務直接的切換

示例:

import time
# import greenlet
from greenlet import greenlet
def f1(s): print(‘第一次f1‘+s) g2.switch(‘taibai‘) #切換到g2這個對象的任務去執行 time.sleep(1) print(‘第二次f1‘+s) g2.switch()
def f2(s): print(‘第一次f2‘+s) g1.switch() time.sleep(1) print(‘第二次f2‘+s)
g1 = greenlet(f1) #實例化一個greenlet對象,並將任務名稱作為參數參進去 g2 = greenlet(f2) g1.switch(‘alex‘) #執行g1對象裏面的任務

  

3.gevent模塊

安裝

pip3 install 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的返回值 

示例:

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

def f1():
    print(‘第一次f1‘)
    # print(threading.current_thread().getName())
    # gevent.sleep(1)
    time.sleep(2)
    print(‘第二次f1‘)

def f2():
    # print(threading.current_thread().getName())
    print(‘第一次f2‘)
    # gevent.sleep(2)
    time.sleep(2)
    print(‘第二次f2‘)

s = time.time()
g1 = gevent.spawn(f1) #異步提交了f1任務
g2 = gevent.spawn(f2) #異步提交了f2任務
# g1.join()
# g2.join()
gevent.joinall([g1,g2])
e = time.time()
print(‘執行時間:‘,e-s)
print(‘主程序任務‘)

 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()  
    print(‘eat food 1‘)
    time.sleep(2)  #加上mokey就能夠識別到time模塊的sleep了
    print(‘eat food 2‘)

def play():
    print(‘play 1‘)
    time.sleep(1)  #來回切換,直到一個I/O的時間結束,這裏都是我們個gevent做得,不再是控制不了的操作系統了。
    print(‘play 2‘)

g1=gevent.spawn(eat)
g2=gevent.spawn(play_phone)
gevent.joinall([g1,g2])
print(‘主‘)

  我們可以用threading.current_thread().getName()來查看每個g1和g2,查看的結果為DummyThread-n,即假線程,虛擬線程,其實都在一個線程裏面

  進程線程的任務切換是由操作系統自行切換的,你自己不能控制

  協程是通過自己的程序(代碼)來進行切換的,自己能夠控制,只有遇到協程模塊能夠識別的IO操作的時候,程序才會進行任務切換,實現並發效果,如果所有程序都沒有IO操作,那麽就基本屬於串行執行了。

python網絡編程--協程