1. 程式人生 > >Python3-協程

Python3-協程

進程 應用程序 stream 返回 time() 執行 系統 初始化 方式

一 引子

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

cpu正在運行一個任務,會在兩種情況下切走去執行其他的任務(切換由操作系統強制控制),

一種情況是該任務發生了阻塞,

另外一種情況是該任務計算的時間過長或有一個優先級更高的程序替代了它

PS:進程的三種執行狀態:運行-阻塞-就緒,線程才是執行單位,所以線程執行狀態跟進程一樣。

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

#1 yiled可以保存狀態,yield的狀態保存與操作系統的保存線程狀態很像,但是yield是代碼級別控制的,更輕量級
#2 send可以把一個函數的結果傳給另外一個函數,以此實現單線程內程序之間的切換  
技術分享圖片
‘‘‘
1、協程:
    單線程實現並發
    在應用程序裏控制多個任務的切換+保存狀態
    優點:
        應用程序級別速度要遠遠高於操作系統的切換
    缺點:
        多個任務一旦有一個阻塞沒有切,整個線程都阻塞在原地
        該線程內的其他的任務都不能執行了

        一旦引入協程,就需要檢測單線程下所有的IO行為,
        實現遇到IO就切換,少一個都不行,以為一旦一個任務阻塞了,整個線程就阻塞了,
        其他的任務即便是可以計算,但是也無法運行了

2、協程序的目的: 想要在單線程下實現並發 並發指的是多個任務看起來是同時運行的 並發=切換+保存狀態 ‘‘‘ #串行執行 import time def func1(): for i in range(10000000): i+1 def func2(): for i in range(10000000): i+1 start = time.time() func1() func2() stop = time.time() print(stop - start) #基於yield並發執行 import time def func1():
while True: yield def func2(): g=func1() for i in range(10000000): i+1 next(g) start=time.time() func2() 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)

yield不能檢測IO,實現遇到IO自動切換
yield

對於單線程下,我們不可避免程序中出現io操作,但如果我們能在自己的程序中(即用戶程序級別,而非操作系統級別)控制單線程下的多個任務能在一個任務遇到io阻塞時就切換到另外一個任務去計算,這樣就保證了該線程能夠最大限度地處於就緒態,即隨時都可以被cpu執行的狀態,相當於我們在用戶程序級別將自己的io操作最大限度地隱藏起來,從而可以迷惑操作系統,讓其看到:該線程好像是一直在計算,io比較少,從而更多的將cpu的執行權限分配給我們的線程。

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

#1. 可以控制多個任務之間的切換,切換之前將任務的狀態保存下來,以便重新運行時,可以基於暫停的位置繼續執行。

#2. 作為1的補充:可以檢測io操作,在遇到io操作的情況下才發生切換

二 協程介紹

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

需要強調的是:

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

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

優點:

#1. 協程的切換開銷更小,在線程內完成切換,操作系統完全感知不到,因而更加輕量級
#2. 單線程內就可以實現並發的效果,最大限度地利用cpu

缺點:

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

總結協程特點:

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

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

  3.在線程內完成協程的切換

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

# 多線程是在操作系統級別的切換, 協程是在程序級別的切換

三 Greenlet

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

from greenlet import greenlet

def eat(name):
    print(%s eat 1 %name)
    g2.switch(egon)
    print(%s eat 2 %name)
    g2.switch()
def play(name):
    print(%s play 1 %name)
    g1.switch()
    print(%s play 2 %name)

g1=greenlet(eat)
g2=greenlet(play)

g1.switch(egon)#可以在第一次switch時傳入參數,以後都不需要

單純的切換(在沒有io的情況下或者沒有重復開辟內存空間的操作),反而會降低程序的執行速度

import time
def f1():
    res=1
    for i in range(100000000):
        res+=i

def f2():
    res=1
    for i in range(100000000):
        res*=i

start=time.time()
f1()
f2()
stop=time.time()
print(run time is %s %(stop-start)) #10.985628366470337

#切換
from greenlet import greenlet
import time
def f1():
    res=1
    for i in range(100000000):
        res+=i
        g2.switch()

def f2():
    res=1
    for i in range(100000000):
        res*=i
        g1.switch()

start=time.time()
g1=greenlet(f1)
g2=greenlet(f2)
g1.switch()
stop=time.time()
print(run time is %s %(stop-start)) # 52.763017892837524

greenlet只是提供了一種比generator更加便捷的切換方式,當切到一個任務執行時如果遇到io,那就原地阻塞,仍然是沒有解決遇到IO自動切換來提升效率的問題。

單線程裏的這20個任務的代碼通常會既有計算操作又有阻塞操作,我們完全可以在執行任務1時遇到阻塞,就利用阻塞的時間去執行任務2。。。。如此,才能提高效率,這就用到了Gevent模塊。

四 Gevent介紹

Gevent跟Greenlet相比,Gevent遇到IO操作可以自動切換,效率提升了和代碼量減少了。

#用法
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是異步提交任務的,如果不join線程不會等協程執行完畢就結束了,協程也會跟著結束。 #或者上述兩步合作一步:gevent.joinall([g1,g2]) g1.value#拿到func1的返回值

遇到IO阻塞時會自動切換任務

技術分享圖片
import gevent
def eat(name):
    print(%s eat 1 %name)
    gevent.sleep(2)
    print(%s eat 2 %name)

def play(name):
    print(%s play 1 %name)
    gevent.sleep(1)
    print(%s play 2 %name)


g1=gevent.spawn(eat,egon)
g2=gevent.spawn(play,name=egon)
g1.join()
g2.join()
#或者gevent.joinall([g1,g2])
print()
View Code

上例gevent.sleep(2)模擬的是gevent可以識別的io阻塞,

而time.sleep(2)或其他的阻塞,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(name):
    print(%s eat 1 % name)
    time.sleep(2)
    print(%s eat 2 % name)

def play(name):
    print(%s play 1 % name)
    time.sleep(3)
    print(%s play 2 %name)

g1 = gevent.spawn(eat, egon)
g2 = gevent.spawn(play, egon)
g1.join()
g2.join()
print(main)

我們可以用threading.current_thread().getName()來查看每個g1和g2,查看的結果為DummyThread-n,即假線程

五 Gevent之應用舉例

通過gevent實現單線程下的socket並發

技術分享圖片
from gevent import monkey
monkey.patch_all()
import socket
import gevent

def communicate(conn):
    while True:
        data = conn.recv(1024)
        if not data:
            break
        conn.send(data.upper())

def server(ip,port):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((ip,port))
    server.listen(5)
    while True:
        conn, addr = server.accept()
        gevent.spawn(communicate, conn)
    server.close()

if __name__ == __main__:
    g = gevent.spawn(server, localhost, 9090)
    g.join()
server 技術分享圖片
from threading  import Thread
import socket
import threading

def client(server_ip, port):
    c = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 套接字對象一定要加到函數內,即局部名稱空間內, 放在函數外面被所有線程共享, 則大家公用一個套接字對象,那麽客戶端端口就永遠一樣了
    c.connect((server_ip, port))

    count = 0
    while True:
        c.send((%s say hello %s % (threading.current_thread().getName(), count)).encode(utf-8))
        msg = c.recv(1024)
        print(msg.decode(utf-8))
        count += 1

if __name__ == __main__:
    for i in range(500):
        t = Thread(target=client, args=(localhost, 9090))
        t.start()
多並發多個客戶端

本文轉載自:http://www.cnblogs.com/linhaifeng/articles/7429894.html

Python3-協程