對Python協程的理解

前言

之前看Python教程的時候瞭解了一些協程的概念,相對還是比較膚淺,但是協程對Python語言而言是一個很重要的特性,加上近期看了我司架構師標哥的一篇講協程的文章,感覺豁然開朗。

爲什麼需要協程

協程這東西,不是Python獨有的,在很多其他腳本語言比如Lua也有,協程的存在,讓單線程跑出了併發的效果,對計算資源的利用率高,開銷小。但是說起來和Python解釋器的設計也有關係,Python的多線程並不支持多核,因爲Python的線程雖然是真正的線程,但解釋器執行代碼時,有一個GIL鎖:Global Interpreter Lock,任何Python線程執行前,必須先獲得GIL鎖,然後,每執行100條字節碼,解釋器就自動釋放GIL鎖,讓別的線程有機會執行。這個GIL全局鎖實際上把所有線程的執行代碼都給上了鎖,所以,多線程在Python中只能交替執行,即使100個線程跑在100核CPU上,也只能用到1個核。

GIL是Python解釋器設計的歷史遺留問題,通常我們用的解釋器是官方實現的CPython,要真正利用多核,除非重寫一個不帶GIL的解釋器。

實現一個協程

協程是一種用戶態的輕量級線程。本篇主要研究協程的C/C++的實現。

首先我們可以看看有哪些語言已經具備協程語義:

  • 比較重量級的有C#、erlang、golang*
  • 輕量級有python、lua、javascript、ruby
  • 還有函數式的scala、scheme等。

目前看到大概有四種實現協程的方式:

第一種:利用glibc 的 ucontext組件(雲風的庫)
第二種:使用匯編代碼來切換上下文(實現c協程)
第三種:利用C語言語法switch-case的奇淫技巧來實現(Protothreads)
第四種:利用了 C 語言的 setjmp 和 longjmp( 一種協程的 C/C++ 實現,要求函數裏面使用 static local 的變量來保存協程內部的數據)

這裏有一個兄弟已經使用ucontext來實現簡單的協程庫(http://blog.csdn.net/qq910894904/article/details/41911175),我就不Copy了。

可以看出來,協程相對線程而言,有一定的相似性,它是藉助用戶空間的上下文切換調度來達到調用者與被調用者之間多次協同的目的。但是調度的主動權卻在用戶,以下是進程,線程,協程的一個對比。
這裏寫圖片描述

協程最大的優勢就是協程極高的執行效率。因爲子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯。

第二大優勢就是不需要多線程的鎖機制,因爲只有一個線程,也不存在同時寫變量衝突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多。

因爲協程是一個線程執行,那怎麼利用多核CPU呢?最簡單的方法是多進程+協程,既充分利用多核,又充分發揮協程的高效率,可獲得極高的性能。

協程的使用場景

前面介紹了協程的概念,但是,協程在什麼情況下使用呢?協程既然誕生了,總有它的理由。前面我們看到,一個線程內的多個協程是串行執行的,不能利用多核,所以,顯然,協程不適合計算密集型的場景。那麼,協程適合什麼場景呢?

異步非阻塞式I/O。

I/O 本來是阻塞的(相較於 CPU 的時間世界而言),就目前而言,無論 I/O 的速度多塊,也比不上 CPU 的速度,所以一個 I/O 相關的程序,當其在進行 I/O 操作時,CPU 實際上是空閒的。我們假設這樣的場景: 1個線程有5個 I/O 相關的事情(子程序)要處理。如果我們絕對的串行化,那麼當其中一個 I/O 阻塞時,其他4個 I/O 並不能得到執行,因爲程序是絕對串行的,5個 I/O 必須一個一個排隊等候處理,當一個 I/O 阻塞時,其它四個也得在那傻等着。如下圖所示:
這裏寫圖片描述

而協程則能比較好地解決這個問題,當一個協程(特殊的子程序)阻塞時,它可以切換到其他沒有阻塞的協程上去繼續執行,這樣就能得到比較高的效率。如下圖所示:

這裏寫圖片描述

還有一個簡單的例子證明協程的實用性。假設你有一個生產者-消費者的關係,這裏一個協程生產產品並將它們加入隊列,另一個協程從隊列中取出產品並使用它。爲了提高效率,你想一次增加或刪除多個產品。代碼可能是這樣的:

var q := new queue

生產者協程

loop
       while q is not full
           create some new items
           add the items to q
       yield to consume

消費者協程

  loop
       while q is not empty
           remove some items from q
           use the items
       yield to produce

詳細比較:
因爲相對於子例程,協程可以有多個入口和出口點,可以用協程來實現任何的子例程。事實上,正如Knuth所說:“子例程是協程的特例。”

每當子例程被調用時,執行從被調用子例程的起始處開始;然而,接下來的每次協程被調用時,從協程返回(或yield)的位置接着執行。

因爲子例程只返回一次,要返回多個值就要通過集合的形式。這在有些語言,如Forth裏很方便;而其他語言,如C,只允許單一的返回值,所以就需要引用一個集合。相反地,因爲協程可以返回多次,返回多個值只需要在後繼的協程調用中返回附加的值即可。在後繼調用中返回附加值的協程常被稱爲產生器。

Python協程庫Eventlet

前文也講了,協程在異步IO上是能提升很多效率的,Python在這塊就有一個針對異步IO的協程庫Eventlet,eventlet是一個用來處理和網絡相關的Python庫函數,而且可以通過協程來實現併發,在eventlet裏,把“協程”叫做greenthread(綠色線程)。所謂併發,就是開啓了多個greenthread,並且對這些greenthread進行管理,以實現非阻塞式的I/O。比如說用eventlet可以很方便的寫一個性能很好的web服務器,或者是一個效率很高的網頁爬蟲,這都歸功於eventlet的“綠色線程”,以及對“綠色線程”的管理機制。更讓人不可思議的是,eventlet爲了實現“綠色線程”,竟然對python的和網絡相關的幾個標準庫函數進行了改寫,並且可以以補丁(patch)的方式導入到程序中,因爲python的庫函數只支持普通的線程,而不支持協程,eventlet稱之爲“綠化”。

這裏要注意的是Eventlet的使用場景,因爲異步IO的多個協程之間的調度相對而言規則比較簡單,所以其調度是由Eventlet裏的Hub組件完成的,而完全定製化的由用戶來做調度,並不能使用Eventlet

Eventlet API分析

1. Greenthread Spawn 生成函數

(1)Greenthread Spawn(spawn,孵化的意思,即如何產生greenthread)
主要有3個函數可以創建綠色線程:

    1)spawn(func, *args, **kwargs):

創建一個綠色線程去運行func這個函數,後面的參數是傳遞給這個函數的參數。返回值是一個eventlet.GreenThread對象,這個對象可以用來接受func函數運行的返回值。在綠色線程池還沒有滿的情況下,這個綠色線程一被創建就立刻被執行。其實,用這種方法去創建線程也是可以理解的,線程被創建出來,肯定是有一定的任務要去執行,這裏直接把函數當作參數傳遞進去,去執行一定的任務,就好像標準庫中的線程用run()方法去執行任務一樣。

    2)spawn_n(func, *args, **kwargs):

這個函數和spawn()類似,不同的就是它沒有返回值,因而更加高效,這種特性,使它也有存在的價值。

    3)spawn_after(seconds, func, *args, **kwargs)

這個函數和spawn()基本上一樣,都有一樣的返回值,不同的是它可以限定在什麼時候執行這個綠色線程,即在seconds秒之後,啓動這個綠色線程。

2. Greenthread Control 協程控制函數

    1)sleep(seconds=0)

中止當前的綠色線程,以允許其它的綠色線程執行。

    2)eventlet.GreenPool 
        starmap(self, function, iterable)和imap(self, function, *iterables)

這是一個類,在這個類中用set集合來容納所創建的綠色線程,並且可以指定容納線程的最大數量(默認是1000個),它的內部是用Semaphore和Event這兩個類來對池進行控制的,這樣就構成了線程池。

Starmap和imap這兩個函數和標準的庫函數中的這兩個函數實現的功能是一樣的,所不同的是這裏將這兩個函數的執行放到了綠色線程中。前者實現的是從iterable中取出每一項作爲function的參數來執行,後者則是分別從iterables中各取一項,作爲function的參數去執行。

    3)eventlet.GreenPile

這也是一個類,而且是一個很有用的類,在它內部維護了一個GreenPool對象和一個Queue對象。這個GreenPool對象可以是從外部傳遞進來的,也可以是在類內部創建的,GreenPool對象主要是用來創建綠色線程的,即在GreenPile內部調用了GreenPool.spawn()方法。而Queue對象則是用來保存spawn()方法的返回值的,即Queue中保存的是GreenThread對象。並且它還實現了next()方法,也就意味着GreenPile對象具有了迭代器的性質。所以如果我們要對綠色線程的返回值進行操作的話,用這個類是再好不過的了。

    4)eventlet.Queue

說到隊列就不得不畫個類圖了,基類是LightQueue,它實現了大部分的隊列的常用方法。它是用collections做爲實現隊列的基本數據結構的。而且這個LightQueue的實現,不單單實現了存取操作,我覺得在本質上它實現了一個生產者和消費者問題,定義了兩個set()類型的成員變量putters和getters,前者用來存放在隊列滿時,被阻塞的綠色線程,後者用來存放當隊列空時,被阻塞的綠色線程。類中的putting()和getting()方法就是分別得到被阻塞的綠色線程的數量。
Queue繼承了LightQueue,並且又增加了它自己的兩個方法:task_done()和join()。task_done()是被消費者的綠色線程所調用的,表示在這個項上的所有工作都做完了,join()是阻塞,直到隊列中所有的任務都完成。LifoQueue和PriorityQueue是存放數據的兩種不同的方式。
這裏寫圖片描述

3. Network Convenience Functions(和網絡相關的函數)

這些函數定義在convenience.py文件中,對和socket相關的網絡通信進行了包裝,注意,這裏用的socket是經過修改後的socket,以使它使用綠色線程,主要有以下一個函數:

      1connect(addr, family=socket.AF_INET, bind=None)

主要執行了以下幾個步驟:新建了一個TCP類型的socket,綁定本地的ip和端口,和遠程的地址進行連接,源碼如下:

def connect(addr, family=socket.AF_INET, bind=None):  
    sock = socket.socket(family, socket.SOCK_STREAM)  
    if bind is not None:  
        sock.bind(bind)  
    sock.connect(addr)  
    return sock
     2listen(addr, family=socket.AF_INET, backlog=50)

過程和connect()類似,只是把connect()換成了listen(),backlog指定了最大的連接數量,源碼如下:

def listen(addr, family=socket.AF_INET, backlog=50):  
    sock = socket.socket(family, socket.SOCK_STREAM)  
    if sys.platform[:3]=="win":  
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 
    sock.bind(addr)  
    sock.listen(backlog)  
    return sock  
     3)serve(sock, handle, concurrency=1000)

這個函數直接創建了一個socket服務器,在它內部創建了一個GreenPool對象,默認的最大綠色線程數是1000,然後是一個循環來接受連接,源碼如下:

def serve(sock, handle, concurrency=1000):  
    pool = greenpool.GreenPool(concurrency)  
    server_gt = greenthread.getcurrent()  

    while True:  
        try:  
            conn, addr = sock.accept()  
            gt = pool.spawn(handle, conn, addr)  
            gt.link(_stop_checker, server_gt, conn)  
            conn, addr, gt = None, None, None  
        except StopServe:  
            return  
 4)wrap_ssl(sock, *a, **kw)

給socket加上ssl(安全套接層),對數據進行加密。
還有幾個比較重要的API這裏就不羅列了,等以後用到了再進行分析吧,下面看幾個官方的例子:

4. Use Case

(1)官方上引以爲傲的“網頁爬蟲”,用到了綠色線程池和imap()函數

urls = ["http://www.google.com/intl/en_ALL/images/logo.gif",  
     "https://wiki.secondlife.com/w/images/secondlife.jpg",  
     "http://us.i1.yimg.com/us.yimg.com/i/ww/beta/y3.gif"]  

import eventlet  
from eventlet.green import urllib2    

def fetch(url):  
  print "opening", url  
  body = urllib2.urlopen(url).read()  
  print "done with", url  
  return url, body  

pool = eventlet.GreenPool(200)  
for url, body in pool.imap(fetch, urls):  
  print "got body from", url, "of length", len(body)  

(2)socket服務器

import eventlet  

def handle(fd):  
    print "client connected"  
    while True:  
        # pass through every non-eof line  
        x = fd.readline()  
        if not x: break  
        fd.write(x)  
        fd.flush()  
        print "echoed", x,  
    print "client disconnected"  

print "server socket listening on port 6000"  
server = eventlet.listen(('0.0.0.0', 6000))  
pool = eventlet.GreenPool()  
while True:  
    try:  
        new_sock, address = server.accept()  
        print "accepted", address  
        pool.spawn_n(handle, new_sock.makefile('rw'))  
    except (SystemExit, KeyboardInterrupt):  
        break  

(3)使用GreenPile的例子

import eventlet  
from eventlet.green import socket  

def geturl(url):  
    c = socket.socket()  
    ip = socket.gethostbyname(url)  
    c.connect((ip, 80))  
    print '%s connected' % url  
    c.sendall('GET /\r\n\r\n')  
    return c.recv(1024)  

urls = ['www.google.com', 'www.yandex.ru', 'www.python.org']  
pile = eventlet.GreenPile()  
for x in urls:  
    pile.spawn(geturl, x)  

# note that the pile acts as a collection of return values from the functions  
# if any exceptions are raised by the function they'll get raised here  
for url, result in zip(urls, pile):  
    print '%s: %s' % (url, repr(result)[:50])  

參考資料:
http://eventlet.net/doc/index.html
http://blog.csdn.net/hackerain/article/details/7836993
http://mp.weixin.qq.com/s?__biz=MzAwNDAxOTM5Mw==&mid=508479846&idx=5&sn=ee86e3914c01bf290b6da1a61b96e343&chksm=00882cc837ffa5de480c3b7674832d182562bf35e78eac9eb8094c5001093efd20bbfb631126&mpshare=1&scene=23&srcid=0712c6k1wJ1QCYISc3m4m8FP#rd

發佈了31 篇原創文章 · 獲贊 1 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章