原創文章出自公眾號:「碼農富哥」,如需轉載請請注明出處!
文章如果對你有收獲,可以收藏轉發,這會給我一個大大鼓勵喲!另外可以關注我公眾號「碼農富哥」 (搜索id:coder2025),我會持續輸出Python,算法,計算機基礎的 原創 文章
協程定義:
協程,又稱微線程,纖程。英文名Coroutine。
子程序,或者稱為函數,在所有語言中都是層級調用,比如A調用B,B在執行過程中又調用了C,C執行完畢返回,B執行完畢返回,最后是A執行完畢。
所以子程序調用是通過棧實現的,一個線程就是執行一個子程序。
子程序調用總是一個入口,一次返回,調用順序是明確的。而協程的調用和子程序不同。
協程看上去也是子程序,但執行過程中,在子程序內部可中斷,然后轉而執行別的子程序,在適當的時候再返回來接著執行。
注意,在一個子程序中中斷,去執行其他子程序,不是函數調用,有點類似CPU的中斷。
那和多線程比,協程有何優勢?
最大的優勢就是協程極高的執行效率。因為子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯。
第二大優勢就是不需要多線程的鎖機制,因為只有一個線程,也不存在同時寫變量沖突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多。
因為協程是一個線程執行,那怎么利用多核CPU呢?最簡單的方法是多進程+協程,既充分利用多核,又充分發揮協程的高效率,可獲得極高的性能。
Python對協程的支持還非常有限,用在generator中的yield可以一定程度上實現協程。雖然支持不完全,但已經可以發揮相當大的威力了。
來看例子:
傳統的生產者-消費者模型是一個線程寫消息,一個線程取消息,通過鎖機制控制隊列和等待,但一不小心就可能死鎖。
如果改用協程,生產者生產消息后,直接通過yield跳轉到消費者開始執行,待消費者執行完畢后,切換回生產者繼續生產,效率極高:
# yield 的生產者消費者模型
def consumer():
r = 'i am start now!!!'
while True:
i = yield r
print 'consuming task %s' % i
r = '200 Done'
def producer(c):
start_up = c.next() # 或者c.send(None) 啟動生成器, 遇到yield 返回 重新來到這里
print 'start_up is %s' % start_up
n = 5
i = 0
while i < n:
i+=1
print 'producing task is %s' % i
res = c.send(i) # 生產了一個任務,通過 send(i) 把函數執行權切換到consumer,消費者接收任務處理, 此時consumer 的yield r 表達式等于send()的參數,即i=i
# 而send(i) 的返回值就由consumer的yield r產生,yield r 可以相當于return r 所以,res=“200 Done”
print 'consumer done ,res: %s' % res
c.close() # 不生產任務了,就關閉生成器
c = consumer()
producer(c)
上面的協程運行流程是:
- c = consumer() 創建一個生成器,注意并不是執行一個函數,這里只會生成一個生成器對象,沒有執行里面的任何代碼, 要啟動生成器,需要c.next() 或者c.send(None)來啟動。
- produce(c) 把c生成器傳進去producer,然后start_up=c.next() 這里就是啟動了consumer()生成器,
啟動的意思是,開始運行生成器,直到遇到yield , 就會把yield后面的內容返回,并且回到原來的地方,
這里遇到了yield r, 相當于把r變量返回(想象成return r), 并且回到執行c.next()的函數(producer)來,繼續執行producer的代碼。
所以start_up = c.next() 做了兩件事:- 進去了另外一個函數consumer()執行, 直到遇到yield
- 遇到 yield r, 然后就回到producer里面了,順便把r的值給到start_up, 這里是start_up="i am start now!!!"
- 保留現場,這次停留在哪個yield, 下次回來就會在這個yield繼續
- 上面啟動了consumer生成器后回到來,producer()繼續跑下面的代碼, 遇到res=c.send(1),
這里的c.send() 相當于 c.next(),也是重新回到生成器里面執行 ,只不過send()可以帶參數,把參數帶過去生成器。
所以 res = c.send(1), 做了幾件事:- 重新進入consumer()生成器,回到上一次跳出來的yield位置, 也就是yield r
- 并且不同于直接c.next(), c.send(1)帶了一個參數1, 而 consumer 里面的i =yield r, 那這次回來就會讓yield r 這個表達式賦值成參數 1, 也就是i=yield r 變成了 i = 1, 這就是send(1) 的作用,傳遞參數
- i = 1后,繼續再生成器consumser 里面執行代碼, print 'consuming task %s' % i 就會輸出 consuming task 1
- 讓 r = '200 Done', 在循環來到 yield r , 相當于yield '200 Done', 返回200 Done ,并且跳出生成器,回到producer()繼續執行。
生產者消費者模式就這樣,通過協程交替循環工作。如果不用協程的話,一個線程,要么做生產者,要么做消費者,不能讓他們切換工作,只能使用多線程,分別運行生產者,消費者。
當然,雖然協程可以切換運行,但畢竟它只有一個線程,只能在代碼之前來回切換運行,不能有并行運行。
總結:
來到這里, 就有點像tornado 異步協程的模型了: producer()類似IOLoop一直在循環,由它來產生事件,再跳出來consumer(),當讓可以有N個consumer(),讓他們處理。
producer()就是一個調度器,可以控制事件扔給哪個協程去處理, 因為協程可以隨時切回來頂級調度器。比如我們可以設定當i是偶數給consumer1()處理, 奇數給consumer2()處理,都是可以的,讓producer()作為一個頂級調度器
tornado 協程
從上面可以看到,Generator已經具備協程的一些能力。如:能夠暫停執行,保存狀態;能夠恢復執行;能夠異步執行。
但是此時Generator還不是一個協程。一個真正的協程能夠控制代碼什么時候繼續執行。而一個Generator執行遇到一個yield表達式 或者語句,會將執行控制權轉移給調用者。
在維基百科中提到,可以實現一個頂級的調度子例程,將執行控制權轉移回Generator,從而讓它繼續執行。在tornado中,ioLoop就是這樣的頂級調度子例程,每個協程模塊通過,函數裝飾器coroutine和ioLoop進行通信,從而ioLoop可以在協程模塊執行暫停后,在合適的時機重新調度協程模塊執行。
不過,接下來還不能介紹coroutine和ioLoop,在介紹這兩者之前,先得明白tornado中在協程環境中一個非常重要的類Future.
類比
就好比上面的producer()作為一個頂級生產者,調度器,可以分配任務給任何消費者生成器,適當時候在執行暫停后,在合適的時機重新調度協程模塊執行。
Future類
Future封裝了異步操作的結果。實際是它類似于在網頁html前端中,圖片異步加載的占位符,但加載后最終也是一個完整的圖片。Future也是同樣用處,tornado使用它,最終希望它被set_result,并且調用一些回調函數。Future對象實際是coroutine函數裝飾器和IOLoop的溝通使者,有著非常重要的作用。
參考 tornado協程(coroutine)原理
異步非阻塞例子
class GenHandler(tornado.web.RequestHandler):
@gen.coroutine
def get(self):
url = 'http://www.baidu.com'
http_client = AsyncHTTPClient()
response = yield http_client.fetch(url)
s = yield gen.sleep(5) # 該阻塞還是得阻塞, 異步只是對其他鏈接而言的
self.write(response.body)
class MainHandler(tornado.web.RequestHandler):
...
if __name__ == "__main__":
application = tornado.web.Application([
(r"/", MainHandler),
(r"/gen_async/", GenHandler),
], autoreload=True)
application.listen(8889)
tornado.ioloop.IOLoop.current().start()
上面的GenHandler便是使用了協程異步的例子, 當我們請求/gen_async/時,這個請求會請求百度網頁和sleep 5秒,當然這個gen.sleep()是非阻塞的。但是我們請求這個url還是會停止5秒后才完成響應。
然而 在這個5秒等待中,我們請求 / 還是tornado還是能直接給我們響應,而不是要等5秒過后才能響應!
這里要強調的是:這里的異步非阻塞是針對另一請求來說的,本次的請求該是阻塞的仍然是阻塞的。
那么我們來分析一下tornado是如何在單線程情況下,一個請求被阻塞,另外的請求還可以處理響應,實現異步的。
首先我們先分析比較簡單的yield gen.sleep(5):
-
查看@gen.coroutine這個裝飾器源碼,看他工作原理
- 首先result=func(*args, **kwargs) 相當于獲得這個生成器對象, 也就是def get()這個生成器,但是也只是返回生成器而已,沒有執行里面任何代碼,需要next()或者send()才會啟動生成器
- 來到 yielded = next(result) 這里就是啟動了生成器,來到get() 里面的yield ,然后返回yield后面的內容,也就是gen.sleep(5) , 返回這個函數,也就是執行這個函數gen.sleep() 然后獲取gen.sleep()里面的返回值, 交給yield, 所以假如gen.sleep(5) 函數最后執行的結果是return 5 ,那執行完以后就yield gen.sleep() 相當于yield 5 所以yield gen.sleep(5)是yield 這個表達式的返回值,跟yield 一個定值是一樣的,只不過要執行完gen.sleep() 才會得到這個定值!
所以當yield gen.sleep()的時候,就是進去執行了這個異步函數gen.sleep()。所以我們要進去看這個gen.sleep()究竟做了什么, 我們進去看看代碼:
def sleep(duration):
f = Future()
IOLoop.current().call_later(duration, lambda: f.set_result(None))
return f
- 首先可以確定的是,這個gen.sleep()返回值是一個future()對象。那么上面的yielded = next(result),實際上被賦值的就是這個future()對象
- 其次,給IOLOOP循環通過add_timeout() 添加了一個callback, 這個add_timeout()本質上就是add_callback, 只是指定多少秒后執行這個callback, 所以這一步最核心的功能就是給ioloop添加一個5秒后執行的callback, 而這個callback就是匿名函數, 執行f.set_result(None), 讓future.set_result(),完成這個future的填充。ioloop會在每次循環檢查執行這些callback,由于 ioloop添加的callback設定了時間,所以在5秒后的循環會執行這個函數。并不是下面說的,ioloop在5秒后再添加callback,而是立即添加了callback,設定了5秒后執行
注意: ioloop不會判斷是否該執行callback, 它只會在每次循環中都執行callback, 所以是否執行callback,應該交給future來決定,所以就有了future.add_done_callback( lambda future: self.add_callback(callback, future))
這樣的函數,意思就是當future 完成后給ioloop添加callback。ioloop只管執行callback, future管啥時候添加callback給ioloop。
Coroutine和IoLoop是如何切換的(之前一直想不明白)
class GenAsyncHandler(RequestHandler):
@gen.coroutine
def get(self):
http_client = AsyncHTTPClient()
response = yield http_client.fetch("http://example.com")
do_something_with_response(response)
self.render("template.html")
- 這里有三個函數概念:
- 裝飾器函數 a() 對應上面的@gen.coroutine()
- 被包裹的函數 b() 對應上面的get()
- 被包裹函數里面的異步執行函數 c() 對應上面的 http_client.fetch(url)
-
Coroutine和IoLoop是如何切換的(之前一直想不明白) 先看看@gen.coroutine是干嘛的
參考上面截圖,他就是一個裝飾器,裝飾器是一個返回函數的高階函數,所以被這個gen.coroutine裝飾器包裹的函數b()都是相當于返回了另外一個函數a(),而b()只是在a()函數的一部分:
"""返回生成器, 還沒啟動生成器"""
result = func(*args, kwargs)
"""啟動生成器, 如果func()里面是 yield gen.sleep(4) 或者 yield async.fetch(url)
那么yielded 就被賦值成他們的返回值,而他們一般是返回future對象
這里實際上就跳進去執行了gen.sleep()或者async.fetch()"""
yielded = next(result)
"""將這個yielded 放進去runner,runner的作用實際上就是進去注冊一個當future完成的callback, 就是run()
run()就是執行gen.send()的地方"""
Runner(result, future, yielded)
- 所以當執行到
yielded = next(result)
時,是進去到b()函數的yield地方,進去yield后面的函數執行, 也就是異步函數c() ,(當然這個c()函數只是給ioloop添加一個callback,并不是阻塞同步執行)并且停止跳出來a(),然后繼續a()函數其他代碼,當yield 的時候,這時運行完c(), 已經從b()跳出來到a()函數了(也就是回到主線程IOloop的循環, 只要不是進去子協程,都是主循環)。
所以這個高階裝飾器比較難理解, 這個裝飾器實際上是另外一個函數a(), 包含了被包裹的原來函數b(), 而在這個高階函數里面,執行原來的函數b(),也就是生成器,都是在裝飾器這個新的函數a()里面操作的。直到遇到func()也就是執行b(), next啟動生成器b()。
遇到yield,執行yield 的 c(),獲得c()的返回值。這樣就取出c()的返回值,
出來到高階函數,也就是從原函數b()停止了。
yielded 取出了異步函數c()的返回值, 比如yield fetch() yield gen.sleep() 的返回值,也就是future占位符,隨后拿著這個占位符,去runner()那里注冊: 當future完成后,執行run()函數, run()的作用就是gen.send(),可以重新回到原函數!
問:那么,什么時候是future被完成呢?也就是什么時候被future.set_result()。是我們需要知道的。
答:當我們yield c()的時候,不就是進去這個c()函數執行嗎, 就是這個時候,在c()里面,注冊了一個函數給ioloop,讓它下次循環執行,執行完自然就會set_result()拉,然后就會再下次循環中知道對應run()執行,發送gen.send(value)這個結果給原函數!
最后在看一個例子:
import tornado.ioloop
from tornado.gen import coroutine
from tornado.concurrent import Future
@coroutine
def get_web():
#http_client = AsyncHTTPClient()
http_client = HTTPClient()
response = yield http_client.fetch("http://example.com")
print 'status_code is %s' % response.code
@coroutine
def asyn_sum(a, b):
print("begin calculate:sum %d+%d"%(a,b))
future = Future()
def callback(a, b):
print("calculating the sum of %d+%d:"%(a,b))
future.set_result(a+b)
tornado.ioloop.IOLoop.instance().add_callback(callback, a, b)
result = yield future
print("after yielded")
print("the %d+%d=%d"%(a, b, result))
def main():
asyn_sum(2,3)
print 'haha'
tornado.ioloop.IOLoop.instance().start()
if __name__ == "__main__":
main()
解析:
- 定義一個callback添加到ioloop里面,這個callback有future.set_result()的功能,相當于上面說的 yield c(), 當執行c()會把這個函數添加到ioloop,然后讓它執行,最后set_result()。 這里只是手動添加而已了。
- 然后定義了一個future , 遇到yield future的時候,進入gen.coroutine裝飾器高階函數,還是按順序執行
result = asyn_sum(2,3)
yielded = next(result)# 把asyn_sum里面定義的future對象拿過來了
future = Future() # 在新建一個新的future 跟上面yielded 這個future是不一樣的
Runner(result, future, yielded) # 把yielded 進去runner注冊, yielded完成后執行runner(),返回到asyn_sum(),
- 所以再高階函數執行完后,asyn_sum(2,3)返回的是新創建的future對象,并且從async_sum() yield那里開始出來。所以這時print 'haha' 然后再print after yielded
- run()函數執行的內容我們看看源碼:
while True:
try:
value = future.result()
yielded = self.gen.send(value)
...
except (StopIteration, Return) as e:
if self.pending_callbacks and not self.had_exception:
...
self.result_future.set_result(_value_from_stopiteration(e))
self.result_future = None
return
...
Tornado異步編程
@gen.coroutine
并不是所有函數加了個裝飾gen.coroutine就會變成異步,比如上面的例子get_web()如果里面的httpclient使用了同步庫HTTPClient(), 再yield http_client.fetch()會報錯
raise BadYieldError("yielded unknown object %r" % (yielded,))
因為這個庫本身是同步的,你yield 的時候,直接執行了這個同步函數,然后返回的是response,給到這個裝飾器的時候,handle_yielded 檢測不到是一個可轉換的future就會報錯,所以要用上異步功能,必須使用符合tornado異步庫要求的庫,它們會把執行的函數添加到ioloop并且返回future,并在完成的時候future.set_result()
@asynchronous
tornado在使用gen.coroutine協程做異步編程之前是用@asynchronous這個裝飾器來異步編程的。
class AsyncHandler(RequestHandler):
@asynchronous
def get(self):
http_client = AsyncHTTPClient()
http_client.fetch("http://example.com",
callback=self.on_fetch)
def on_fetch(self, response):
do_something_with_response(response)
self.render("template.html")
它的原理就是當異步調用 http_client.fetch() 時,進去執行,還是跟上面一樣,沒有同步執行,等待返回,而是創建一個future(), 然后把這個callback on_fetch() 注冊到ioloop里,等這個future()被set_result()了就會執行on_fetch()
而真正要做的request = fetch()也要下達命令執行,由于這個是非阻塞的,由epollIO復用機制通知是否完成, 所以是發起這個請求通過add_handler(fd, callback)來加進ioloop循環,然后ioloop等待epoll的通知,fd有數據可讀了,說明請求完成, 就可以執行fetch()的callback,通過源碼知道,這個callback是:給上面創建的future set_result。
意思就是當請求完了,EPOLL通知ioloop, ioloop執行這個fd的callback,也就是設置這個請求完成了,future.set_result()
def handle_response(response):
if raise_error and response.error:
future.set_exception(response.error)
else:
future.set_result(response)
- 一旦這個future.set_result()了,就會執行第一步給ioloop添加的callback, 也就是我們在代碼上寫的on_fetch()函數。 這個過程行云流水。
所以無論是用callback()方式, 還是gen.corountin來實現異步,他們的核心都是一樣,通過add_callback,和非阻塞任務add_handler()來注冊任務或者非阻塞socket。
參考
tornado協程(coroutine)原理
真正的 Tornado 異步非阻塞
淺析tornado協程運行原理
廖雪峰的官方網站-協程
廖雪峰的官方網站-裝飾器
現代魔法學院
最后
原創文章出自公眾號:「碼農富哥」,如需轉載請請注明出處!
文章如果對你有收獲,可以收藏轉發,這會給我一個大大鼓勵喲!另外可以關注我公眾號「碼農富哥」 (搜索id:coder2025),我會持續輸出Python,算法,計算機基礎的 原創 文章