Python_協程

来源:https://www.cnblogs.com/Golanguage/archive/2020/03/02/12398022.html
-Advertisement-
Play Games

引子 之前我們學習了線程、進程的概念,瞭解了在操作系統中進程是資源分配的最小單位,線程是CPU調度的最小單位。按道理來說我們已經算是把cpu的利用率提高很多了。但是我們知道無論是創建多進程還是創建多線程來解決問題,都要消耗一定的時間來創建進程、創建線程、以及管理他們之間的切換。 隨著我們對於效率的追 ...


引子

  之前我們學習了線程、進程的概念,瞭解了在操作系統中進程是資源分配的最小單位,線程是CPU調度的最小單位。按道理來說我們已經算是把cpu的利用率提高很多了。但是我們知道無論是創建多進程還是創建多線程來解決問題,都要消耗一定的時間來創建進程、創建線程、以及管理他們之間的切換。

  隨著我們對於效率的追求不斷提高,基於單線程來實現併發又成為一個新的課題,即只用一個主線程(很明顯可利用的cpu只有一個)情況下實現併發。這樣就可以節省創建線進程所消耗的時間。

  為此我們需要先回顧下併發的本質:切換+保存狀態

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

    

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

   一:其中第二種情況並不能提升效率,只是為了讓cpu能夠雨露均沾,實現看起來所有任務都被“同時”執行的效果,如果多個任務都是純計算的,這種切換反而會降低效率。

  為此我們可以基於yield來驗證。yield本身就是一種在單線程下可以保存任務運行狀態的方法,我們來簡單複習一下:

#1 yiled可以保存狀態,yield的狀態保存與操作系統的保存線程狀態很像,但是yield是代碼級別控制的,更輕量級
#2 send可以把一個函數的結果傳給另外一個函數,以此實現單線程內程式之間的切換
#串列執行
import time
def consumer(res):
    '''任務1:接收數據,處理數據'''
    pass

def producer():
    '''任務2:生產數據'''
    res=[]
    for i in range(10000000):
        res.append(i)
    return res

start=time.time()
#串列執行
res=producer()
consumer(res) #寫成consumer(producer())會降低執行效率
stop=time.time()
print(stop-start) #1.5536692142486572



#基於yield併發執行
import time
def consumer():
    '''任務1:接收數據,處理數據'''
    while True:
        x=yield

def producer():
    '''任務2:生產數據'''
    g=consumer()
    next(g)
    for i in range(10000000):
        g.send(i)

start=time.time()
#基於yield保存狀態,實現兩個任務直接來回切換,即併發的效果
#PS:如果每個任務中都加上列印,那麼明顯地看到兩個任務的列印是你一次我一次,即併發執行的.
producer()

stop=time.time()
print(stop-start) #2.0272178649902344
單純地切換反而會降低運行效率

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

import time
def consumer():
    '''任務1:接收數據,處理數據'''
    while True:
        x=yield

def producer():
    '''任務2:生產數據'''
    g=consumer()
    next(g)
    for i in range(10000000):
        g.send(i)
        time.sleep(2)

start=time.time()
producer() #併發執行,但是任務producer遇到io就會阻塞住,並不會切到該線程內的其他任務去執行

stop=time.time()
print(stop-start)
yield無法做到遇到io阻塞

  對於單線程下,我們不可避免程式中出現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模塊

安裝 :pip3 install greenlet

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時傳入參數,以後都不需要
greenlet實現狀態切換

單純的切換(在沒有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模塊

安裝:pip3 install gevent

Gevent 是一個第三方庫,可以輕鬆通過gevent實現併發同步或非同步編程,在gevent中用到的主要模式是Greenlet, 它是以C擴展模塊形式接入Python的輕量級協程。 Greenlet全部運行在主程式操作系統進程的內部,但它們被協作式地調度。

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
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('')
例:遇到io主動切換

上例gevent.sleep(2)模擬的是gevent可以識別的io阻塞,而time.sleep(2)或其他的阻塞,gevent是不能直接識別的需要用下麵一行代碼,打補丁,就可以識別了

from gevent import monkey;monkey.patch_all()

import gevent
import time
def eat():
    print('eat food 1')
    time.sleep(2)
    print('eat food 2')

def play():
    print('play 1')
    time.sleep(1)
    print('play 2')

g1=gevent.spawn(eat)
g2=gevent.spawn(play)
gevent.joinall([g1,g2])
print('')
View Code

我們可以用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().getName())
    print('eat food 1')
    time.sleep(2)
    print('eat food 2')

def play():
    print(threading.current_thread().getName())
    print('play 1')
    time.sleep(1)
    print('play 2')

g1=gevent.spawn(eat)
g2=gevent.spawn(play)
gevent.joinall([g1,g2])
print('')
查看threading.current_thread().getName()

 

Gevent之同步與非同步

複製代碼
from gevent import spawn,joinall,monkey;monkey.patch_all()

import time
def task(pid):
    """
    Some non-deterministic task
    """
    time.sleep(0.5)
    print('Task %s done' % pid)


def synchronous():  # 同步
    for i in range(10):
        task(i)

def asynchronous(): # 非同步
    g_l=[spawn(task,i) for i in range(10)]
    joinall(g_l)
    print('DONE')
    
if __name__ == '__main__':
    print('Synchronous:')
    synchronous()
    print('Asynchronous:')
    asynchronous()
#  上面程式的重要部分是將task函數封裝到Greenlet內部線程的gevent.spawn。
#  初始化的greenlet列表存放在數組threads中,此數組被傳給gevent.joinall 函數,
#  後者阻塞當前流程,並執行所有給定的greenlet任務。執行流程只會在 所有greenlet執行完後才會繼續向下走。
複製代碼

 

Gevent之應用舉例

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

註意 :from gevent import monkey;monkey.patch_all()一定要放到導入socket模塊之前,否則gevent無法識別socket的阻塞

from gevent import monkey;monkey.patch_all()
from socket import *
import gevent

#如果不想用money.patch_all()打補丁,可以用gevent自帶的socket
# from gevent import socket
# s=socket.socket()

def server(server_ip,port):
    s=socket(AF_INET,SOCK_STREAM)
    s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
    s.bind((server_ip,port))
    s.listen(5)
    while True:
        conn,addr=s.accept()
        gevent.spawn(talk,conn,addr)

def talk(conn,addr):
    try:
        while True:
            res=conn.recv(1024)
            print('client %s:%s msg: %s' %(addr[0],addr[1],res))
            conn.send(res.upper())
    except Exception as e:
        print(e)
    finally:
        conn.close()

if __name__ == '__main__':
    server('127.0.0.1',8080)
server
from socket import *

client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))


while True:
    msg=input('>>: ').strip()
    if not msg:continue

    client.send(msg.encode('utf-8'))
    msg=client.recv(1024)
    print(msg.decode('utf-8'))
client
from threading import Thread
from socket import *
import threading

def client(server_ip,port):
    c=socket(AF_INET,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=('127.0.0.1',8080))
        t.start()
多線程併發多個客戶端

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • if-else 作為每種編程語言都不可或缺的條件語句,我們在編程時會大量的用到。但if-else 一般不建議嵌套超過三層,如果一段代碼存在過多的 if-else 嵌套,代碼的可讀性就會急速下降,後期維護難度也大大提高。 ...
  • 一、前言 在實時性要求較高的場合中,CPU軟體執行的方式顯然不能滿足需求,這時需要硬體邏輯實現部分功能。要想使自定義IP核被CPU訪問,就必須帶有匯流排介面。ZYNQ採用AXI BUS實現PS和PL之間的數據交互。本文以PWM為例設計了自定義AXI匯流排IP,來演示如何靈活運用ARM+FPGA的架構。 ...
  • 問題描述 伺服器鏡像有寶塔面板 + Python2,用yum和pip3安裝Python3 + Flask + Gunicorn後,寶塔面板打不開了。報錯如下: 報錯早就刷上去了,找不回來,這段照搬別人的[^引用1],因為看著差不了多少。 其實我的情況跟引用1差不多,但是就是最後一步依然報錯。 修複 ...
  • 由於細節內容實在太多啦,所以只把部分知識點截圖出來粗略的介紹,每個小節點裡面都有更細化的內容! 內容簡介 這套書同時收錄了十多位阿裡技術人的訪談:從工程師到合伙人的多隆,6 年時間影響數億用戶的靖世,入選 MIT2017 年度 TR35 的王剛 &吳翰清,免試晉升為研究員的錢磊等,將為你展現不一樣的 ...
  • 當前測試人員的主要矛盾: Python基礎語法都看了學了幾遍了。 用不上,不會寫,網上的例子又大部分是面向開發人員的, 好多都是人工智慧,大數據,區塊鏈,或者數學演算法題。 偶爾有Selenium面向Web測試人員, 但是Selenium作為Python入門實例, 也不合適,因為還是太難了。 武散人的 ...
  • 題目 第一次做全英文編程題(辭彙量嚴重不足,還需多記單詞); 題目的大致意思就是說:輸入一個不超過20位的整數,將這個數乘以2,比較計算前和計算後的結果是否滿足題目的要求, 這個要求具體是指判斷組成這兩個數的具體數字出現的次數是否相等,例如:這個數為123456789,它乘以2之後得到2469135 ...
  • 基本配置 一、創建django程式 終端命令: django-admin startproject sitename IDE創建django程式時,本質上都是自動執行上述命令 其他常用命令:進入工程目錄 python manage.py runserver 127.0.0.1 埠號 運行測試 py ...
  • 資料庫的安裝和連接 PyMySQL的安裝 pip install PyMySQL python連接資料庫 import pymysql db = pymysql.connect("資料庫ip","用戶","密碼","資料庫" ) 打開資料庫連接 cursor.execute("SELECT VERS ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...