非同步編程在python中最近是越來越受歡迎,在python中有著許多libraries是用來做非同步的,其中之一是asyncio而且這也是讓python在async編程受歡迎的主因,在開始正題前,我們先來理解一些歷史緣由。
在普遍的程式,執行順序都是一行一行執行,每次要繼續往下執行前,都會等著上一行完成,也就是俗稱的Sequential programming,那麼這樣的編程可能會遇到什麼問題呢? 最大的問題就是如果上一行執行太久的話,我一定要等上一行執行完我才能夠繼續往下走嗎? 最常見的情況就是api request,得到回傳結果,我才能繼續往下走,但是其實我下面接著要做的並不用等這個結果就可以執行了,所以就會耗費無意義的時間,為了解決這樣的事情,會使用thread。
process 可以產生多個 thread,可以讓你的程式一次做很多事情,把它想成影分身,主體只有一個,但是你的分身卻可以同時幫你做其他事情。
上面這張圖,說明了什麼? 帥! 哈哈,其實是想表達,鳴人自己(process),開出了很多分身(thread),每個分身都做不同的事情
方便吧! 但是 thread 是有他的問題存在的,其中像是
先撇除上面會遇到的問題,thread還有著一個成本就是cpu的context switch,因為一顆cpu一次只能run一個thread,它實際上背後用很快的速度在進行thread的交換並執行,這就是所謂的context switch。那麼會有既可以達到多工的效果,又可以免除遇到上述的race condition等等問題的技術存在嗎?! 答案是有的,那就是今天我們要講的主題 python async io,ayncio背後其實是用到coroutine的概念實作,從wiki上面來看,其實coroutine就是一種可以中斷及繼續執行函式呼叫的技術,直接從下面的例子來看!
In [4]:
import time
def n_hello():
for i in range(6):
print(i)
def c_hello():
for i in range(4):
print('in function {}'.format(i))
yield i
def infinit_loop():
num = 0
while True:
num += 1
print(num)
yield
n_hello()
print("=====")
c = c_hello()
next(c)
print("come back to main")
next(c)
next(c)
Out[4]:
上面就是python最基本支援coroutine的使用方式,第一個function n_hello
是一般的for loop版本的印出數字,另外一個function c_hello
是使用yield,藉此讓你看看兩者行為,明顯的感受出使用yield可以將程式的執行順序從subroutine轉回到main,繼續呼叫next又可以跳回去subroutine。
有沒有覺得跟multi thread很像呢,基本上是行為是差不多的,但是coroutine是基於中斷函式,繼續執行其他函式的方式來達到多工,並不像multi thread,會有同時兩個thread執行同份程式碼的問題,進而造成前面所說的,race condition, dead lock.. 那些問題,前面使用鳴人的影分身來比喻multi thread,對於coroutine,我個人想要使用下面這張來比喻
影子模仿術,鹿丸放出多條影子(coroutine),藉由自己的大腦來控制所有人的行動。
那麼接著再稍微深入看看yield的使用方式,前面使用方式是yield把值從function傳出去,那麼我們今天可以把值從外面傳到function裡面使用嗎? 答案是可以的! 以下看看例子
In [5]:
def g(x):
for i in range(x):
yield i
def will_cause_exception():
x = yield
print("wow {}".format(x))
return x
def infinite_send():
while True:
x = yield
print("send {}".format(x))
w = will_cause_exception()
next(w)
try:
w.send(5)
except StopIteration as e:
return_value = e.value
# the function return value will be store in the exception's value
print(return_value)
# let you see exception
raise e
根據上面的使用情境,你應該會覺得多多少少可以有更方便的用法才對,因此python的確在pep380有提出yield from這個語法糖
In [6]:
def test_yield_from():
w = will_cause_exception()
value = yield from w
print("no exception {}".format(value))
yield
t = test_yield_from()
next(t)
t.send(10)
def amazing_yeild_from(x):
yield from range(x)
yield from range(x-1, -1, -1)
print(list(amazing_yeild_from(5)))
In [8]:
%%time
import asyncio
import requests
@asyncio.coroutine
def aio_requests(url):
r = requests.get(url)
return r
@asyncio.coroutine
def aio_response(response):
data = response.text
return data
urls = ['http://www.google.com', 'http://www.yandex.ru',
'http://www.python.org', 'http://www.python.org', 'http://www.python.org']
@asyncio.coroutine
def call_url(url):
response = yield from aio_requests(url)
data = yield from aio_response(response)
print('{}: {} bytes'.format(url, len(data)))
return data
futures = [call_url(url) for url in urls]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(futures))
In [9]:
%%time
def syn_call_url(url):
r = requests.get(url)
data = r.text
print('{}: {} bytes'.format(url, len(data)))
for url in urls:
syn_call_url(url)
In [ ]:
%%time
async def async_requests(url):
r = requests.get(url)
return r
async def async_response(response):
data = response.text
return data
async def call_url(url):
response = await async_requests(url)
data = await async_response(response)
print('{}: {} bytes'.format(url, len(data)))
return data
futures = [call_url(url) for url in urls]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(futures))
In [ ]:
asyncio 神秘在哪? 讓我們來瞧瞧
https://www.reddit.com/r/learnpython/comments/5qwm5h/asyncio_for_dummies/dd432ke/
golang 沒有 reentrant lock
In [5]:
%%time
import time
time.sleep(1)
wall time才是真的process耗費的時間 cpu times只是純粹程式在cpu裡面實際在跑的時間
python async io ,是基於event loop來進行coroutine切換,