多任務--協程 基於python實現

協程

協程,又稱微線程,纖程。英文名Coroutine。

協程的定義

協程是python箇中另外一種實現多任務的方式,只不過比線程更小佔用更小執行單元(理解爲需要的資源)。 爲啥說它是一個執行單元,因爲它自帶CPU上下文。這樣只要在合適的時機, 我們可以把一個協程 切換到另一個協程, 只要這個過程中保存或恢復 CPU上下文那麼程序還是可以運行的。

通俗的理解:在一個線程中的某個函數,可以在任何地方保存當前函數的一些臨時變量等信息,然後切換到另外一個函數中執行,注意不是通過調用函數的方式做到的,並且切換的次數以及什麼時候再切換到原來的函數都由開發者自己確定。

協程和線程差異

協同程序(coroutine)與多線程情況下的線程比較類似:有自己的堆棧,自己的局部變量,有自己的指令指針(IP,instruction pointer),但與其它協同程序共享全局變量等很多信息。

協程(協同程序): **同一時間只能執行某個協程。**開闢多個協程開銷不大。協程適合對某任務進行分時處理。

線程: **同一時間可以同時執行多個線程。**開闢多條線程開銷很大。線程適合多任務同時處理。

協程,即協作式程序,其思想是,一系列互相依賴的協程間依次使用CPU,每次只有一個協程工作,而其他協程處於休眠狀態。協程實際上是在一個線程中,只不過每個協程對CUP進行分時,協程可以訪問和使用unity的所有方法和component。

線程,多線程是阻塞式的,每個IO都必須開啓一個新的線程,但是對於多CPU的系統應該使用thread,尤其是有大量數據運算的時刻,但是IO密集型就不適合;而且thread中不能操作unity的很多方法和component

線程和協同程序的主要不同在於:在多處理器情況下,從概念上來講多線程程序同時運行多個線程;而協同程序是通過協作來完成,在任一指定時刻只有一個協同程序在運行,並且這個正在運行的協同程序只在必要時纔會被掛起。在實現多任務時, 線程切換從系統層面遠不止保存和恢復 CPU上下文這麼簡單。 操作系統爲了程序運行的高效性每個線程都有自己緩存Cache等等數據,操作系統還會幫你做這些數據的恢復操作。 所以線程的切換非常耗性能。但是協程的切換隻是單純的操作CPU的上下文,所以一秒鐘切換個上百萬次系統都抗的住。

簡單實現協程

import time

def work1():
    while True:
        print("----work1---")
        yield
        time.sleep(0.5)

def work2():
    while True:
        print("----work2---")
        yield
        time.sleep(0.5)

def main():
    w1 = work1()
    w2 = work2()
    while True:
        next(w1)
        next(w2)

if __name__ == "__main__":
    main()

運行結果:

----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
...省略...

greenlet模塊使用

爲了更好使用協程來完成多任務,python中的greenlet模塊對其封裝,從而使得切換任務變的更加簡單。.
安裝方式:

sudo pip3 install greenlet
#coding=utf-8

from greenlet import greenlet
import time

def test1():
    while True:
        print "---A--"
        gr2.switch()
        time.sleep(0.5)

def test2():
    while True:
        print "---B--"
        gr1.switch()
        time.sleep(0.5)

gr1 = greenlet(test1)
gr2 = greenlet(test2)

#切換到gr1中運行
gr1.switch()

運行效果:

---A--
---B--
---A--
---B--
---A--
---B--
---A--
---B--
...省略...

gevent模塊使用

greenlet已經實現了協程,但是這個還的人工切換,是不是覺得太麻煩了,不要捉急,python還有一個比greenlet更強大的並且能夠自動切換任務的模塊gevent。

其原理是當一個greenlet遇到IO(指的是input output 輸入輸出,比如網絡、文件操作等)操作時,比如訪問網絡,就自動切換到其他的greenlet,等到IO操作完成,再在適當的時候切換回來繼續執行。

由於IO操作非常耗時,經常使程序處於等待狀態,有了gevent爲我們自動切換協程,就保證總有greenlet在運行,而不是等待IO

安裝:pip3 install gevent

gevent的使用

import gevent

def f(n):
    for i in range(n):
        print(gevent.getcurrent(), i)

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()

運行結果

<Greenlet at 0x10e49f550: f(5)> 0
<Greenlet at 0x10e49f550: f(5)> 1
<Greenlet at 0x10e49f550: f(5)> 2
<Greenlet at 0x10e49f550: f(5)> 3
<Greenlet at 0x10e49f550: f(5)> 4
<Greenlet at 0x10e49f910: f(5)> 0
<Greenlet at 0x10e49f910: f(5)> 1
<Greenlet at 0x10e49f910: f(5)> 2
<Greenlet at 0x10e49f910: f(5)> 3
<Greenlet at 0x10e49f910: f(5)> 4
<Greenlet at 0x10e49f4b0: f(5)> 0
<Greenlet at 0x10e49f4b0: f(5)> 1
<Greenlet at 0x10e49f4b0: f(5)> 2
<Greenlet at 0x10e49f4b0: f(5)> 3
<Greenlet at 0x10e49f4b0: f(5)> 4

可以看到,3個greenlet是依次運行而不是交替運行,這說明當程序中沒有等待或者延時過程的時候,和普通的函數沒有什麼差別,因爲協程是單程運行的。

import gevent

def f(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        #用來模擬一個耗時操作,注意不是time模塊中的sleep
        gevent.sleep(1)

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()

這個時候在函數f(n)中加上一個 gevent.sleep(1)延時函數,在這個延時的過程中,系統不會等待在這個函數中而是切換到其他的函數中運行,直到這個等待結束在切換回來。

運行結果:

<Greenlet at 0x7fa70ffa1c30: f(5)> 0
<Greenlet at 0x7fa70ffa1870: f(5)> 0
<Greenlet at 0x7fa70ffa1eb0: f(5)> 0
<Greenlet at 0x7fa70ffa1c30: f(5)> 1
<Greenlet at 0x7fa70ffa1870: f(5)> 1
<Greenlet at 0x7fa70ffa1eb0: f(5)> 1
<Greenlet at 0x7fa70ffa1c30: f(5)> 2
<Greenlet at 0x7fa70ffa1870: f(5)> 2
<Greenlet at 0x7fa70ffa1eb0: f(5)> 2
<Greenlet at 0x7fa70ffa1c30: f(5)> 3
<Greenlet at 0x7fa70ffa1870: f(5)> 3
<Greenlet at 0x7fa70ffa1eb0: f(5)> 3
<Greenlet at 0x7fa70ffa1c30: f(5)> 4
<Greenlet at 0x7fa70ffa1870: f(5)> 4
<Greenlet at 0x7fa70ffa1eb0: f(5)> 4

這樣就有一個問題,我們之前使用的延時函數是time包中的sleep現在要使用 gevent.sleep()纔可實現線程,這不就意味着我們之前寫的代碼中存在的延時阻塞的函數都要改爲gevent的延時和阻塞函數,如果代碼量很大的,這樣就給工作造成了麻煩,有沒有什麼好的方法可以直接把我們之前的代碼中的阻塞和延時函數自動轉化爲gevent包的阻塞和延時函數了,辦法當然有的。

給程序打補丁:

from gevent import monkey
import gevent
import random
import time

def coroutine_work(coroutine_name):
    for i in range(10):
        print(coroutine_name, i)
        time.sleep(random.random())

gevent.joinall([
        gevent.spawn(coroutine_work, "work1"),
        gevent.spawn(coroutine_work, "work2")
])

可以引入gevent包的monkey 在程序的開頭使用monkey.patch_all() 這語句 ,這樣程序便會自動檢查代碼中的延時和阻塞的函數,自動的進行的轉化。
測試代碼:

from gevent import monkey
import gevent
import random
import time

# 有耗時操作時需要
monkey.patch_all()  # 將程序中用到的耗時操作的代碼,換爲gevent中自己實現的模塊

def coroutine_work(coroutine_name):
    for i in range(10):
        print(coroutine_name, i)
        time.sleep(random.random())

gevent.joinall([
        gevent.spawn(coroutine_work, "work1"),
        gevent.spawn(coroutine_work, "work2")
])
work1 0
work2 0
work1 1
work1 2
work1 3
work2 1
work1 4
work2 2
work1 5
work2 3
work1 6
work1 7
work1 8
work2 4
work2 5
work1 9
work2 6
work2 7
work2 8
work2 9

進程、線程、協程對比

進程,線程,協程可以使用這樣的一個比喻來表示他們的不同。
有一個老闆想要開個工廠進行生產某件商品(例如剪子)
他需要花一些財力物力(電腦的資源0)製作一條生產線,這個生產線上有很多的器件以及材料這些所有的 爲了能夠生產剪子而準備的資源稱之爲:進程。
只有生產線是不能夠進行生產的,所以老闆的找個工人來進行生產,這個工人能夠利用這些材料最終一步步的將剪子做出來(一個流水線不一定只有一個工人,一個進程不僅僅只有一個線程),這個來做事情的工人稱之爲:線程
這個老闆爲了提高生產率,想到3種辦法:

  1. 在這條生產線上多招些工人,一起來做剪子,這樣效率是成倍増長,即單進程 多線程方式。
  2. 老闆發現這條生產線上的工人不是越多越好,因爲一條生產線的資源以及材料畢竟有限,所以老闆又花了些財力物力購置了另外一條生產線,然後再招些工人這樣效率又再一步提高了,即多進程多線程方式
  3. 老闆發現,現在已經有了很多條生產線,並且每條生產線上已經有很多工人了(即程序是多進程的,每個進程中又有多個線程),爲了再次提高效率,老闆想了個損招,規定:如果某個員工在上班時臨時沒事或者再等待某些條件(比如等待另一個工人生產完謀道工序之後他才能再次工作),那麼這個員工就利用這個時間去做其它的事情,那麼也就是說:如果一個線程等待某些條件,可以充分利用這個時間去做其它事情,其實這就是:協程方式.

簡單總結

  1. 進程是資源分配的單位
  2. 線程是操作系統調度的單位
  3. 進程切換需要的資源很最大,效率很低
  4. 線程切換需要的資源一般,效率一般(當然了在不考慮GIL的情況下)
  5. 協程切換任務資源很小,效率高
  6. 多進程、多線程根據cpu核數不一樣可能是並行的,但是協程是在一個線程中 所以是併發

案例:併發下載器

併發下載原理

from gevent import monkey
import gevent
import urllib.request

# 有耗時操作時需要
monkey.patch_all()

def my_downLoad(url):
    print('GET: %s' % url)
    resp = urllib.request.urlopen(url)
    data = resp.read()
    print('%d bytes received from %s.' % (len(data), url))

gevent.joinall([
        gevent.spawn(my_downLoad, 'http://www.baidu.com/'),
        gevent.spawn(my_downLoad, 'http://www.itcast.cn/'),
        gevent.spawn(my_downLoad, 'http://www.itheima.com/'),
])

運行結果

GET: http://www.baidu.com/
GET: http://www.itcast.cn/
GET: http://www.itheima.com/
111327 bytes received from http://www.baidu.com/.
172054 bytes received from http://www.itheima.com/.
215035 bytes received from http://www.itcast.cn/.

從上能夠看到是先發送的獲取baidu的相關信息,然後依次是itcast、itheima,但是收到數據的先後順序不一定與發送順序相同,這也就體現出了異步,即不確定什麼時候會收到數據,順序不一定。
實現多個視頻下載:

from gevent import monkey
import gevent
import urllib.request

#有IO才做時需要這一句
monkey.patch_all()

def my_downLoad(file_name, url):
    print('GET: %s' % url)
    resp = urllib.request.urlopen(url)
    data = resp.read()

    with open(file_name, "wb") as f:
        f.write(data)

    print('%d bytes received from %s.' % (len(data), url))

gevent.joinall([
        gevent.spawn(my_downLoad, "1.mp4", 'http://oo52bgdsl.bkt.clouddn.com/05day-08-%E3%80%90%E7%90%86%E8%A7%A3%E3%80%91%E5%87%BD%E6%95%B0%E4%BD%BF%E7%94%A8%E6%80%BB%E7%BB%93%EF%BC%88%E4%B8%80%EF%BC%89.mp4'),
        gevent.spawn(my_downLoad, "2.mp4", 'http://oo52bgdsl.bkt.clouddn.com/05day-03-%E3%80%90%E6%8E%8C%E6%8F%A1%E3%80%91%E6%97%A0%E5%8F%82%E6%95%B0%E6%97%A0%E8%BF%94%E5%9B%9E%E5%80%BC%E5%87%BD%E6%95%B0%E7%9A%84%E5%AE%9A%E4%B9%89%E3%80%81%E8%B0%83%E7%94%A8%28%E4%B8%8B%29.mp4'),
])

上面的url可以換爲自己需要下載視頻、音樂、圖片等網址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章