一. 協程
我對協程的理解就是在單執行緒中執行函式A,可以中斷函式A的執行,切換去執行其他函式,在合適的時候(由程式自己控制)在切換回A繼續執行,達到類似多執行緒的效果。
這樣做的好處是:
- 沒有執行緒的切換,所以不會有執行緒切換的開銷。
- 因為只有一個執行緒,所以修改共享的變數不需要鎖
- 協程之間的切換由程式控制而不是作業系統控制
二. 協程的效率
-
計算密集型
在計算密集型的程式中,協程的效率並不是很高,反而會變低:
from threading import Thread import time import queue def work(q): res = 0 for i in range(1000000): res += i ** 2 q.put(res) def mutil_thread(): res = 0 threads = [] q = queue.Queue() for i in range(2): t = Thread(target=work, args=(q,)) t.start() threads.append(t) [thread.join() for thread in threads] while not q.empty(): res += q.get() print(res) def coroutine_consumer(): n = 0 while True: m = yield n if not isinstance(m, int): return n += m ** 2 def coroutine_producer(consumer): consumer.send(None) for i in range(2): for i in range(1000000): res = consumer.send(i) consumer.close() print(res) def normal(): res = 0 for i in range(2): for j in range(1000000): res += j ** 2 print(res) t1 = time.time() normal() print('單執行緒:%fs' % (time.time() - t1)) t2 = time.time() mutil_thread() print('多執行緒:%fs' % (time.time() - t2)) t3 = time.time() coroutine_producer(coroutine_consumer()) print('協程:%fs' % (time.time() - t3)) 複製程式碼
結果:
可以看到協程是最慢的
-
IO密集型
import time import asyncio import threading def work(t): print('task %d start' % t) time.sleep(t) # 想象這是一個費時的io操作 print('task %d end' % t) def normal(): [work(i) for i in range(1, 3)] def multi_thread(): threads = [] for i in range(1, 3): t = threading.Thread(target=work, args=(i,)) t.start() threads.append(t) [thread.join() for thread in threads] async def work_coroutine(t): print('task %d start' % t) await asyncio.sleep(t) # 想象這是一個費時的io操作 print('task %d end' % t) async def main(loop): tasks = [loop.create_task(work_coroutine(t)) for t in range(1, 3)] await asyncio.wait(tasks) t1 = time.time() normal() print('單執行緒:%fs' % (time.time() - t1)) t2 = time.time() loop = asyncio.get_event_loop() loop.run_until_complete(main(loop)) loop.close() print('協程:%fs' % (time.time() - t2)) t3 = time.time() multi_thread() print('多執行緒:%fs' % (time.time() - t3)) 複製程式碼
結果:
協程是和多執行緒的速度差不多的,但是協程是在一個執行緒裡實現的,沒有執行緒切換的開銷,所以執行緒越多,協程的效能優勢就越明顯。
三. 關於生成器的一些總結
之前在生成器和迭代器中總結過這方面的一些知識,但是看到協程的使用,好多大神都會用使用了yield的函式作為例子講解,但是我並不是很理解。所以在這裡繼續總結一些之前沒有總結到的知識點:
-
生成器的執行順序
我一直以為使用了yield的函式(也就是生成器),是執行到了yield返回yield後面的跟著的值,然後繼續執行下面的程式碼,然後第二次執行的時候再從頭開始,其實順序不是這樣的,是第一次執行到yield處,然後返回yield後面的跟著的值,函式就暫停了,等下一次再執行的時候就從上一次暫停的yield處繼續執行,例子:
def my_generator(max): n = 0 while n <= max: print('yield前') yield n print('yield後') n += 1 g = my_generator(10) print(next(g)) print('=' * 10) print(next(g)) 複製程式碼
結果:
可以看到第一次使用next獲取迭代器g的值得時候,是到了yield前然後返回,就停了,第二次則是從上次停止的地方繼續執行下面的程式碼,碰到yield返回,函式暫停。
-
next,send,close,throw
生成器有幾種狀態,可以通過getgeneratorstate方法看到:
GEN_CREATED # 等待執行 GEN_RUNNING # 正在執行 GEN_SUSPENDED # 暫停(在yield處) GEN_CLOSED # 執行結束 複製程式碼
當生成器第一次被呼叫的時候是沒有辦法拿到yield返回的結果的:
from inspect import getgeneratorstate def my_generator(max): n = 0 while n <= max: yield n n += 1 g = my_generator(10) print(g) # <generator object my_generator at 0x102804d68> print(getgeneratorstate(g)) # GEN_CREATED 複製程式碼
想要拿到yield返回的結果,就要用的send或者next方法,相當於啟用(啟動)生成器。例子:
def my_generator(max): n = 0 while n < max: m = yield n n += m g = my_generator(10) print(next(g)) # 0 print(g.send(4)) # 4 print(g.send(8)) # 8 複製程式碼
next和send方法都可以拿到生成器yield返回的結果,它們的不同就是send可以帶一個引數,這個引數指的是上一次yield語句的返回值。分析一下上面的例子就明白了:
# 生成器 def my_generator(max): n = 0 while n < max: m = yield n n += m # 建立一個generator物件,並沒有執行yield語句 g = my_generator(10) ''' 啟用生成器,並執行它,執行到yield n,返回n,注意 m = yield n的執行順序是從右到左,也就是yield n之後函式就暫停了 m這時候並沒有被賦值。這時候n = 0 ''' print(next(g)) ''' send 方法指定了一個上次 yield n 的返回值為4,這已經是第二次執行生成器了 所以從上次停止的地方開始,上次是停在了給m賦值的操作,yield n返回值被指定為 4,所以m被賦值為4,繼續執行n += m這句程式碼,這時候n = 4, while語句的條件為True, 所以繼續迴圈到了 m = yield n這句程式碼,同樣從右到左的順序,執行 yield n 返回n的值4 ''' print(g.send(4)) ''' 以此類推,上次還還是停在了給m賦值的操作,send方法的引數是8,所以上次 yield n 這句程式碼返回的就是8,把8賦值給m,繼續執行,當再一次執行到m = yield n的時候,n就是12了。 ''' print(g.send(8)) 複製程式碼
如果上面解釋的還是不清楚的話,可以這樣理解,就是 yield n 這句話會讓生成器返回 n的值,然後yield n這句話本身也會返回一個值,send方法的引數就是指定yield n這句話本身的返回值。
有時候還會看到這樣的程式碼:
example_generator.send(None) 複製程式碼
send(None)就好像第一次呼叫生成器的next(example_generator)一樣,都可以啟用一個生成器,只不過啟用的時候send的引數只能是None。例子:
def my_generator(max): n = 0 while n < max: m = yield n n = m g = my_generator(100) # g.send(None) == next(g) print(g.send(None)) # 0 print(g.send(4)) # 4 print(g.send(8)) # 12 複製程式碼
close方法就和它的名字一樣,關閉一個生成器。當關閉之後再通過next或者send方法就會報StopIteration。例子:
def my_generator(max): n = 0 while n < max: m = yield n n = m g = my_generator(100) print(g.send(None)) print(g.send(4)) g.close() print(g.send(8)) 複製程式碼
# 結果 0 4 Traceback (most recent call last): File "test.py", line 16, in <module> print(g.send(8)) StopIteration 複製程式碼
throw方法可以結束生成器執行,並丟擲指定異常或系統定義異常
def my_generator(max): try: n = 0 while n < max: m = yield n n = m except ValueError: print('ValueError') g = my_generator(100) print(g.send(None)) # 0 print(g.send(4)) # 4 g.throw(ValueError) # ValueError 複製程式碼
四. asyncio
說了那麼多,終於到了asyncio,asyncio是Python內建的一個標準庫,是Python3.4版本之後引入的,也就是Python內建了對非同步IO的支援。
asyncio的使用:
-
async/await 關鍵字
給函式前面加上 async 關鍵字可以定義一個協程,直接呼叫這個協程的函式,並不會直接執行這個函式,而是返回一個coroutine物件,而且還會引發一個RuntimeWarning的警告
import asyncio async def say_hello(): print('Hello') await asyncio.sleep(1) 複製程式碼
# 結果 <coroutine object say_hello at 0x10e797ec8> test.py:8: RuntimeWarning: coroutine 'say_hello' was never awaited print(say_hello()) RuntimeWarning: Enable tracemalloc to get the object allocation traceback 複製程式碼
await 則是掛起一個耗時的操作,也就是告訴Python這一步是耗時的,主執行緒不用等待這個操作,可以切換去執行其他協程。
例子:
import asyncio async def say_hello(i): print('Hello start %d ' % i) await asyncio.sleep(1) print('Hello end %d' % i) async def main(loop): tasks = [loop.create_task(say_hello(i)) for i in range(1, 3)] await asyncio.wait(tasks) loop = asyncio.get_event_loop() loop.run_until_complete(main(loop)) 複製程式碼
結果:
Hello start 1 Hello start 2 Hello end 1 Hello end 2 複製程式碼
使用await,主執行緒就不會去等待asyncio.sleep(1)這個操作了,它會轉去執行其他協程。只有協程, Task 和 Future才可以被await可以成功掛起,比如
await time.sleep(1)
就不會被掛起 -
asyncio.sleep
asyncio.sleep有點像time.sleep,但是time.sleep會阻塞主執行緒,這個不會,它會阻塞當前的任務,讓Python去執行其他的任務,引數就阻塞的時間。
-
loop.create_task
說loop.create_task之前,最好先知道這兩個概念:
Future物件: 包含了非同步操作結果的物件。
Task 物件:繼承自Future,它是對Future和協程的進一步封裝,用於事件迴圈。
我這個是抄別人部落格中的解釋,原部落格地址
說實話還是不理解這個兩個概念,目前的理解是,協程函式不能直接執行,會報警告(之前的例子中有),想要執行協程函式就得將它包裝成Task 物件。包裝的方法有兩個:
- asyncio.ensure_future(coro_or_future, *, loop=None)
- loop.create_task(coroutine)
官方推薦使用 loop.create_task,ensure_future接受future物件或者協程物件,create_task只接受協程物件。
-
asyncio.wait
asyncio.wait是同時執行任務的方法,和它很像的還有一個asyncio.gather。看下它們兩的區別:
import asyncio async def foo(i): return i ** i async def work_1(loop): tasks = [asyncio.create_task(foo(i)) for i in range(4)] done, pending = await asyncio.wait(tasks) print(done, pending) for task in done: print(task.result()) loop = asyncio.get_event_loop() loop.run_until_complete(work_1(loop)) 複製程式碼
# 結果: {<Task finished coro=<foo() done, defined at test.py:4> result=1>, <Task finished coro=<foo() done, defined at test.py:4> result=1>, <Task finished coro=<foo() done, defined at test.py:4> result=4>, <Task finished coro=<foo() done, defined at test.py:4> result=27>} set() 1 1 4 27 複製程式碼
- 第一個引數是包含 coroutines 或 futures 的可迭代物件。
- 執行是無序的。
- 返回值是完成的任務和未完成的任務,通過result方法獲取任務的結果
import asyncio async def foo(i): return i ** i async def work_2(loop): tasks = [asyncio.create_task(foo(i)) for i in range(4)] result = await asyncio.gather(*tasks) print(result) loop = asyncio.get_event_loop() loop.run_until_complete(work_2(loop)) 複製程式碼
# 結果: [1, 1, 4, 27] 複製程式碼
- 第一個引數是任意個 coroutines 或 futures。
- 執行是有序的
- 返回值是已完成任務的結果
-
loop = asyncio.get_event_loop
得到當前上下文的事件迴圈。
這句話一下子搞了兩個對我來說很模糊的概念,就是事件迴圈和上下文:
-
事件迴圈:
在計算系統中,可以產生事件的實體叫做事件源,能處理事件的實體叫做事件處理者。此外,還有一些第三方實體叫做事件迴圈。它的作用是管理所有的事件,在整個程式執行過程中不斷迴圈執行,追蹤事件發生的順序將它們放到佇列中,當主執行緒空閒的時候,呼叫相應的事件處理者處理事件。
-
上下文:
上下文是一段程式執行所需要的最小資料集合。
在協程中,把協程函式註冊(放入)事件迴圈中,當事件發生的時候就會呼叫對應的協程函式。
-
-
loop.run_until_complete
執行直到傳入的Future物件完成。run_until_complete方法可以接受Future物件也可以接受協程物件,如果傳入的是協程物件,它會幫你轉換成Future物件。
-
loop.close()
關閉事件迴圈。
基本上是把例子中用到的都說了,雖然還是有點不清楚,先記下來,下一步就是應用在爬蟲上,希望可以在用的時候加深理解。