【Python协程的实现】
"
補(bǔ)充:數(shù)據(jù)安全問題
進(jìn)程: 多個進(jìn)程操作同一個文件,會出現(xiàn)數(shù)據(jù)不安全線程: 多個線程操作同一個全局變量,會出現(xiàn)數(shù)據(jù)不安全 對于共享的數(shù)據(jù)操作: 如果是 += *= /= -= 操作,都存在數(shù)據(jù)不安全問題 如果是append,extend,pop,remove操作,就不會出現(xiàn)數(shù)據(jù)不安全問題協(xié)程: 永遠(yuǎn)不會出現(xiàn)數(shù)據(jù)不安全問題 因?yàn)閰f(xié)程是由程序員控制的,而程序員控制的只能是代碼協(xié)程示例代碼:
# 最簡單的協(xié)程 a = 0 def fn1(): global a g = fn2() # 拿到生成器 next(g) # 轉(zhuǎn)向fn2函數(shù)執(zhí)行 a += 1 next(g) # 轉(zhuǎn)向fn2函數(shù)執(zhí)行 def fn2(): global a yield a += 1 yield print(fn1()) # Noneprint(a) # 2
1. 協(xié)程介紹
協(xié)程是單線程下的并發(fā),又稱微線程,纖程。英文名Coroutine。一句話說明什么是協(xié)程:協(xié)程是一種用戶態(tài)的輕量級線程,即協(xié)程是由用戶程序自己控制調(diào)度的.
1. Python的線程屬于內(nèi)核級別的,即由操作系統(tǒng)控制調(diào)度(如單線程遇到io或執(zhí)行時間過長就會被迫交出cpu執(zhí)行權(quán)限,切換其它線程運(yùn)行)
2. 單線程內(nèi)開啟協(xié)程,一旦遇到io,就會從應(yīng)用程序級別(而非操作系統(tǒng))控制切換,以此來提升效率(非io操作的切換反而會降低效率!)
對比操作系統(tǒng)控制線程的切換,用戶在單線程內(nèi)控制協(xié)程的切換
優(yōu)點(diǎn)如下:
1. 協(xié)程的切換開銷更小,屬于程序級別的切換,操作系統(tǒng)完全感知不到,因而更加輕量級
2. 單線程內(nèi)就可以實(shí)現(xiàn)并發(fā)的效果,最大限度地利用cpu
缺點(diǎn)如下:
1. 協(xié)程的本質(zhì)是單線程下實(shí)現(xiàn)并發(fā),因而無法利用多核。(可以是一個程序開啟多個進(jìn)程,每個進(jìn)程內(nèi)開啟多個線程,每個線程內(nèi)開啟協(xié)程)
2. 協(xié)程指的是單個線程,因而一旦協(xié)程出現(xiàn)阻塞,將會阻塞整個線程
總結(jié)協(xié)程特點(diǎn)
1. 必須在一個單線程里實(shí)現(xiàn)并發(fā)
2. 修改共享數(shù)據(jù)不需加鎖
3. 用戶程序里自己保存多個控制流的上下文棧
(附加:一個協(xié)程遇到io操作自動切換到其它協(xié)程(如何實(shí)現(xiàn)檢測io?yield、greenlet都無法實(shí)現(xiàn),需要用到gevent模塊(select機(jī)制)))
二、greenlet模塊
windows安裝命令:pip3 install greenlet
單純的切換(在沒有io或沒有重復(fù)開辟內(nèi)存空間的操作下)反而會降低程序的執(zhí)行速度
# 效率對比 from greenlet import greenletfrom time import time def func1(): res = 1 for i in range(1000000): res +=i def func2(): res = 1 for i in range(1000000): res *=i # 順序執(zhí)行start = time()func1()func2()print('run time is', time() - start)# run time is 0.19996070861816406 # 切換start =time()g1 = greenlet(func1)g2 = greenlet(func2)g1.switch()print('run time is', time() - start)# run time is 19.51878547668457
greenlet只是提供了一種比generator更加便捷的切換方式,當(dāng)切到一個任務(wù)執(zhí)行時,如果遇到io操作,便會原地阻塞,仍然沒有解決遇到io自動切換來提升效率的問題.
單線程里的多個任務(wù)通常會既有計算操作又有阻塞操作,我們完全可以在執(zhí)行任務(wù)1遇到阻塞時就切換到任務(wù)2繼續(xù)執(zhí)行。如此才能提高效率,這就需要用到Gevent模塊.
三、gevent模塊
windows安裝命令:pip3 install gevent
gevent模塊是一個第三方庫,可以輕松通過gevent實(shí)現(xiàn)并發(fā)同步或異步編程,在gevent中用到的主要模式是greenlet,它是以C擴(kuò)展模塊形式接入Python的輕量級協(xié)程。greenlet全部運(yùn)行在主程序操作系統(tǒng)進(jìn)程的內(nèi)部,但它們被協(xié)作式地調(diào)度。
基本用法
g = gevent.spawn(func, 1, 2, x=3, y=4):創(chuàng)建一個協(xié)程對象g,spawn括號內(nèi)的第一個參數(shù)是函數(shù)名,后面可以有多個參數(shù),可以是位置實(shí)參或關(guān)鍵字實(shí)參,都是傳給函數(shù)的func。
g.join():等待g結(jié)束,等價于gevent.joinall([g1, g2])
g.value:拿到func的返回值
# 遇到io主動切換 import gevent def eat(name): print('%s eat 1' % name) gevent.sleep(1) print('%s eat 2' % name) def play(name): print('%s play 1' % name) gevent.sleep(1) print('%s play 2' % name) g1 = gevent.spawn(eat, 'egon')g2 = gevent.spawn(play, name='egon')g1.join()g2.join()# 或者 gevent.joinall([g1, g2])
上面的gevent.sleep()模擬的是gevent可以識別的io阻塞,而time.sleep()或其它的阻塞,gevent是不能直接識別的,需要用到下面一行代碼,打補(bǔ)丁,便可識別:from gevent import monkey; monkey.patch_all() 必須寫在被打補(bǔ)丁者之前:
from gevent import spawn, monkeyimport timedef eat(name): print('%s eat 1' % name) time.sleep(1) print('%s eat 2' % name) def play(name): print('%s play 1' % name) time.sleep(1) print('%s play 2' % name) monkey.patch_all() # 打補(bǔ)丁g1 = spawn(eat, 'egon')g2 = spawn(play, name='egon')g1.join()g2.join()# 或者 gevent.joinall([g1, g2])
我們可以使用threading.current_thread().getName()來查看每個協(xié)程的變量名都會為:DummyThread-n,既假線程。
from gevent import spawn, monkeyfrom threading import current_thread func1 = lambda :print(current_thread().getName()) # DummyThread-1 monkey.patch_all()spawn(func1).join()
同步與異步效率對比
# 同步與異步效率對比 from gevent import spawn, joinall, monkey;monkey.patch_all()from time import sleep def task(pid): """Some non-deterministic task""" sleep(0.5) print('Task %s done' % pid) def synchronous(): # 同步 [task(i) for i in range(10)] def asynchronous(): # 異步 gevent_lst = [spawn(task, i) for i in range(10)] joinall(gevent_lst) print('DONE') if __name__ == '__main__': print('Syinchronous:') synchronous() print('Asynchronous:') asynchronous()
異步應(yīng)用爬蟲
from gevent import spawn, joinall, monkey; monkey.patch_all()from requests import getfrom time import time def get_page(url): print('GET: %s' % url) response = get(url) if response.status_code == 200: print('%d bytes received from %s' %(len(response.text), url)) start_time = time()joinall([ spawn(get_page, 'https://www.python.org/'), spawn(get_page, 'https://www.yahoo.com/'), spawn(get_page, 'https://github.com/'),]) print('run time is %s' %(time() - start_time))
實(shí)例:實(shí)現(xiàn)單線程下的socket并發(fā)
# Server from gevent import spawn, monkey; monkey.patch_all()from socket import socket, SOL_SOCKET, SO_REUSEADDR def server(ip='127.0.0.1', port=8080): sk = socket() sk.setsockopt(SOL_SOCKET, SO_REUSEADDR,1) sk.bind((ip, port)) sk.listen(10) while 1: conn, addr = sk.accept() spawn(task, conn) print('Client', addr) def task(conn): try: while 1: res = conn.recv(1472) if not res:break print(res.decode('UTF-8')) conn.send(res.upper()) except Exception as e: print(e) finally: conn.close() if __name__ == '__main__': server()
# Clinet from socket import socket sk = socket()sk.connect_ex(('127.0.0.1', 8080)) while 1: ret = input('>>>').strip() sk.send(ret.encode('UTF-8')) if not ret:break print(sk.recv(1472).decode('UTF-8'))
關(guān)于yield:
from time import time # 在單線程中,如果存在多個函數(shù),如果有某個函數(shù)發(fā)生IO操作,你想讓程序馬上切換到另一個函數(shù)去執(zhí)行# 以此來實(shí)現(xiàn)一個假的并發(fā)現(xiàn)象。# 總結(jié):# yield 只能實(shí)現(xiàn)單純的切換函數(shù)和保存函數(shù)狀態(tài)的功能# 不能實(shí)現(xiàn):當(dāng)某一個函數(shù)遇到io阻塞時,自動的切換到另一個函數(shù)去執(zhí)行# 目標(biāo)是:當(dāng)某一個函數(shù)中遇到IO阻塞時,程序能自動的切換到另一個函數(shù)去執(zhí)行# 如果能實(shí)現(xiàn)這個功能,那么每個函數(shù)都是一個協(xié)程## 但是 協(xié)程的本質(zhì)還是主要依靠于yield去實(shí)現(xiàn)的。## 如果只是拿yield去單純的實(shí)現(xiàn)一個切換的現(xiàn)象,你會發(fā)現(xiàn),跟本沒有程序串行執(zhí)行效率高 def consumer(): while 1: x = yield print(x) def producer(): g = consumer() next(g) [g.send(i) for i in range(100000)] start = time()producer()print('yield:', time() - start)
"
總結(jié)
以上是生活随笔為你收集整理的【Python协程的实现】的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 四面酱酒为什么停产?
- 下一篇: 机器语言——码运算(具体解释反码补码由来