123 協程基礎

1、線程、進程回顧

  1. 在操做系統中進程是資源分配的最小單位,線程是CPU調度的最小單位。python

  2. 併發的本質:切換+保存狀態。程序員

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

  4. 在介紹進程理論時,說起進程的三種執行狀態,而線程纔是執行單位,因此也能夠將上圖理解爲線程的三種狀態。函數

  5. 其中併發並不能提高效率,只是爲了讓cpu可以雨露均沾,實現看起來全部任務都被「同時」執行的效果,若是多個任務都是純計算的,這種切換反而會下降效率。spa

2、協程介紹

協程:是單線程下的併發,又稱微線程,纖程。英文名Coroutine。操作系統

一句話說明什麼是協程:協程是一種用戶態的輕量級線程,即協程是由用戶程序本身控制調度的,單線程下實現併發。線程

須要強調的是code

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

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

重點:遇到io切換的時候纔有意義進程

具體: 協程概念本質是程序員抽象出來的,操做系統根本不知道協程存在,也就說來了一個線程我本身遇到io 我本身線程內部直接切到本身的別的任務上了,操做系統跟本發現不了,也就是實現了單線程下效率最高.

優勢

  1. 協程的切換開銷更小,屬於程序級別的切換,操做系統徹底感知不到,於是更加輕量級
  2. 單線程內就能夠實現併發的效果,最大限度地利用cpu

缺點

  1. 協程的本質是單線程下,沒法利用多核,能夠是一個程序開啓多個進程,每一個進程內開啓多個線程,每一個線程內開啓協程,本身要檢測全部的io,但凡是有一個阻塞總體都跟着阻塞.
  2. 協程指的是單個線程,於是一旦協程出現一個阻塞,沒有切換任務,將會阻塞整個線程

特色

  1. 必須在只有一個單線程裏實現併發
  2. 修改共享數據不需加鎖
  3. 用戶程序裏本身保存多個控制流的上下文棧
import time
def eat():
    print('eat 1')
    # 瘋狂的計算呢沒有io
    time.sleep(2)
    # for i in range(100000000):
    #     i+1
def play():
    print('play 1')
    # 瘋狂的計算呢沒有io
    time.sleep(3)
    # for i in range(100000000):
    #     i+1
play()
eat() # 5s

在單線程裏,利用yield來實現協程,這是一個沒有意義的攜程(由於咱們說過協程要作在有io的狀況下才有意義)

import time
def func1():
    while True:
        1000000+1
        yield

def func2():
    g = func1()
    for i in range(100000000):
        i+1
        next(g)

start = time.time()
func2()
stop = time.time()
print(stop - start) # 17.68560242652893

對比上面yeild切換運行的時間,反而比咱們單獨取執行函數串行更消耗時間,因此上面實現的攜程是沒有意義的。

import time

def func1():
    for i in range(100000000):
        i+1
def func2():
    for i in range(100000000):
        i+1

start = time.time()
func1()
func2()
stop = time.time()
print(stop - start) # 12.08229374885559

3、協程的本質

協程的本質就是在單線程下,由用戶本身控制一個任務遇到io阻塞了就切換另一個任務去執行,以此來提高效率。爲了實現它,咱們須要找尋一種能夠同時知足如下條件的解決方案:

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

3.1 使用協程咱們須要用到genvent模塊

重點:使用gevent來實現協程是能夠的,可是咱們說過協程最主要是遇到IO纔有意義,可是剛好這個gevent模塊作不到協程的真正的意義,也就是說這個而模塊他檢測不到IO

但用gevent模塊是檢測不到IO的,也就是說這樣寫一樣是沒有意義的

下面程序裏的gevent是一個類

  1. gevent.spawn本質調用了gevent.greenlet.Greenlet的類的靜態方法spawn:

    @classmethod
    def spawn(cls, *args, **kwargs):
        g = cls(*args, **kwargs)
        g.start()
        return g
  2. 這個類方法調用了Greenlet類的兩個函數,*__init_*_ 和 start. init函數中最爲關鍵的是這段代碼: 

    def __init__(self, run=None, *args, **kwargs):
       greenlet.__init__(self, None, get_hub()) # 將新創生的greenlet實例的parent一概設置成hub
       if run is not None:
       self._run = run
# 在這段程序咱們發現,這段程序並無實現碰見IO的時候,用戶模cpu實現任務切換
import gevent
import time

def eat():
    print('eat 1')
    time.sleep(2)
    print('eat 2')
def play():
    print('play 1')
    # 瘋狂的計算呢沒有io
    time.sleep(3)
    print('play 2')

start = time.time()
g1 = gevent.spawn(eat)
g2 = gevent.spawn(play)
g1.join()
g2.join()
end = time.time()
print(end-start) 5.0041022300720215

'''
結果:
eat 1
eat 2
play 1
play 2
5.004306077957153
'''

重點二:使用gevent的一個補丁來實現,經過gevent類來實現真正有意義的協程,用戶真正的實現裏以操做系統發現不了的方式,模擬了碰見IO的時候實現任務之間的來回切換

注意:這裏再次強調,協程的本質意義是在單線程內實現任務的保存狀態加切換,而且真正的協程必須是在遇到IO的狀況

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

def eat():
    print('eat 1')
    time.sleep(2)
    print('eat 2')
def play():
    print('play 1')
    # 瘋狂的計算呢沒有io
    time.sleep(3)
    print('play 2')

start = time.time()
g1 = gevent.spawn(eat)
g2 = gevent.spawn(play)
g1.join()
g2.join()
end = time.time()
print(end-start)# 3.003168821334839

'''
結果:
eat 1
play 1
eat 2
play 2
3.003168821334839
'''
相關文章
相關標籤/搜索