并行關鍵看是否能同時處理多個任務.依靠多核,同一時間每個CPU上執行一個任務
并發關鍵看是否能在一段時間內處理多個任務并不要求同時,
他倆最本質的特點就是是否能同時處理多個任務.無多核無并行
并行指的是多個CPU,并發主要是針對一個CPU而已,多個任務在一個CPU上交替切換任務。
并發的目的:充分利用處理器的每一個核,達到最高的處理性能。
協程線程進程的區別
協程也被稱為微線程,下面對比一下協程和線程:
- 線程之間需要上下文切換成本相對協程來說是比較高的,尤其在開啟線程較多時,但協程的切換成本非常低。
- 同樣的線程的切換更多的是靠操作系統來控制,而協程的執行由我們自己控制
- 不需要線程的的鎖機制
進程 數據隔離 數據不安全 切換開銷最大 操作系統控制 能用多核
線程 數據共享 數據不安全 切換開銷大 操作系統控制 不能用多核
協程 數據共享 數據安全 切換開銷小 用戶控制 不能用多核
協程
引用官方說法:
協程是一種用戶態的輕量級線程,協程的調度完全由用戶控制。協程擁有自己的寄存器上下文和棧。
協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧,直接操作棧則基本沒有內核切換的開銷,可以不加鎖的訪問全局變量,所以上下文的切換非常快。
并發的本質:就是多個任務在一條線程中來回切換并保存狀態,來實現一條線程上的io降到最低
線程和進程都是靠操作系統來控制切換+保存狀態的,而協程的是靠用戶來切換和保存狀態。
既然需要我們自己來控制切換和保存狀態,我們的yield的關鍵字可以解決這個問題,但是有一個關鍵的問題是:什么時候切換?只有遇到io切換才能提高程序效率,
所以協程最關鍵是用來解決單線程實現并發的io問題的。
協程也被稱為''微線程'',協程并不是真實存在的,是程序員自己命名的
cpu正在運行一個任務,會在兩種情況下切走去執行其他的任務(切換由操作系統強制控制),一種情況是該任務發生了阻塞,另外一種情況是該任務計算的時間過長或有一個優先級更高的程序替代了它
協程的本質
協程的本質就是在單線程下,由用戶自己控制一個任務遇到io阻塞了就切換另外一個任務去執行,以此來提升單線程的執行效率。為了實現它,我們需要找尋一種可以同時滿足以下條件的解決方案:
#1. 可以控制多個任務之間的切換,切換之前將任務的狀態保存下來,以便重新運行時,可以基于暫停的位置繼續執行。 #2. 作為1的補充:可以檢測io操作,在遇到io操作的情況下才發生切換
協程的作用
1.減少任務切換的開銷。
2.實現單線程下并發。單線程并發要解決的核心是,解決io,遇到io切換才能提高效率,而對于線程和進程來說io問題已經被操作系統解決了,然后協程需要我們自己來解決io。
3.最主要的問題是解決io,提高了單線程的執行效率
協程的優點
1.切換開銷小,速度快
2.不需要多線程的鎖機制,因為只有一個線程,也不存在同時寫變量沖突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多
3.可以實現單線程下的并發
協程的缺點
-
協程的本質是單線程下,無法利用多核.
-
協程指的是單個線程,因而一旦協程出現阻塞,將會阻塞整個線程
線程相當于協程來說的一個優點?
我們知道線程是靠操作系統的來規避io的,協程是靠用戶規避io的,但是用戶不能規避所有的io,一些文件操作的io(比如print)線程感知比協程更敏感.目前為止協程能感知到的io有請求網絡
greenlet
官網https://greenlet.readthedocs.io/en/latest/
greenlet是一個輕量級的并發編程模塊。它實現了并發但是沒有實現協程,因為協程要求遇到io才切換。
官網例子:
from greenlet import greenlet def test1(): print(12) gr2.switch() #切換到gr2greenlet當調用test2函數 print(34) def test2(): print(56) gr1.switch() print(78) gr1 = greenlet(test1) gr2 = greenlet(test2) gr1.switch() #當綁定的函數有參數時,從這里傳進去
我們首先創建兩個greenlet并分別綁定text1和text2,然后執行switch切換到gr1然后執行text1函數打印出12,然后再切換到gr2打印56,然后再切換到gr1因為greenlet能保存狀態所以執行print(78)這個代碼打印出34,text結束由于此時沒有切換到gr2 78不會被打印出來
結果
12 56 34
greenlet 的模塊和yield并沒有什么區別,在協程中只是單純的來切換任務,并沒有解決效率問題,只有遇到io的時候切換才會有效率
如果我們在單個線程內有20個任務,要想實現在多個任務之間切換,使用yield生成器的方式過于麻煩(需要先得到初始化一次的生成器,然后再調用send。。。非常麻煩),而使用greenlet模塊可以非常簡單地實現這20個任務直接的切換
from greenlet import greenlet import time def eat(name): print('%s eat 1' %name) time.sleep(1) g2.switch('egon') print('%s eat 2' %name) g2.switch() def play(name): print('%s2 play 1' %name) g1.switch() print('%s2 play 2' %name) g1=greenlet(eat) g2=greenlet(play) g1.switch('egon')#可以在第一次switch時傳入參數,以后都不需要
結果:
egon eat 1 egon2 play 1 egon eat 2 egon2 play 2
總結:greenlet只是提供了一種比生成器更加便捷的切換方式,當切到一個任務執行時如果遇到io,那就原地阻塞,仍然是沒有解決遇到IO自動切換來提升效率的問題。
gevent
Gevent 是一個第三方庫,可以通過gevent實現協程,進而實現并發編程,在genent中用到的主要模塊就是greenlet,該模塊用c語言寫的,它比greenlet模塊多了一個可以檢測出io再切換
主要思想是:
當一個greenlet遇到io操作時,比如說訪問網站,就自動切換到其他的greenlet,等待io操作完成再在適當的時候切換回來繼續執行,由于IO操作非常耗時,經常使程序處于等待狀態,有了gevent為我們自動切換協程,就保證總有greenlet在運行,而不是等待IO。
作用:
- 切換+保存狀態
- 自動能檢測到io
- 遇到io切換任務
gevent可以規避哪些io
def patch_all(socket=True, dns=True, time=True, select=True, thread=True, os=True, ssl=True, httplib=False, # 這里就表明了不支持網絡io也就是說請求網頁的io subprocess=True, sys=False, aggressive=True, Event=True, builtins=True, signal=True, queue=True, **kwargs):
from gevent import monkey monkey.patch_all() #這個方法就是為了監聽代碼下邊的所有的io操作,必須寫在代碼的開頭 import gevent import time def eat(name): print('%s eat 1' %name) gevent.sleep(2) #其實就是genvent.sleep可以識別io操作,有io后直接切換到下一個任務 print('%s eat 2' %name) return 'eat' def play(name): print('%s play 1' %name) gevent.sleep(1) print('%s play 2' %name) return 'play' start=time.time() g1=gevent.spawn(eat,"小紅") #創建協程對象,spawn括號內第一個參數是函數名,如eat,后面可以有多個參數,可以是位置實參或關鍵字實參,都是傳給函數eat的 g2=gevent.spawn(play,'小明') #這也是異步提交 g1.join()#等待g1完成 g2.join()#等待g完成 #或者把上面兩步何為一步 gevent.joinall([g1,g2]) print("主線程開始運行",time.time()-start) print(g1.value,g2.value) #拿到上述函數的返回值
結果:
小紅 eat 1 小明 play 1 小明 play 2 小紅 eat 2 主線程開始運行 2.003354787826538 eat play
gevent 的應用
例子1爬蟲
from gevent import monkey;monkey.patch_all() import gevent import requests import time def get_page(url): print('GET: %s' %url) response=requests.get(url) if response.status_code == 200: print('%d bytes received from %s' %(len(response.text),url)) start_time=time.time() gevent.joinall([ gevent.spawn(get_page,'https://www.python.org/'), gevent.spawn(get_page,'https://www.yahoo.com/'), gevent.spawn(get_page,'https://github.com/'), ]) stop_time=time.time() print('run time is %s' %(stop_time-start_time))
結果:
59631 bytes received from https://github.com/ 499780 bytes received from https://www.yahoo.com/ 48823 bytes received from https://www.python.org/ run time is 1.8453788757324219
使用asyncio來實現上邊的爬蟲
import asyncio import aiohttp import time async def get_page(url): async with aiohttp.ClientSession() as session: async with session.get(url) as response: if response.status == 200: content = await response.text() print(f'{len(content)} bytes received from {url}') async def main(): tasks = [ get_page('https://www.python.org/'), get_page('https://www.yahoo.com/'), get_page('https://github.com/'), ] await asyncio.gather(*tasks) if __name__ == '__main__': start_time = time.time() asyncio.run(main()) stop_time = time.time() print('run time is %s' % (stop_time - start_time))
例子2 socket的服務端和客戶端的并發
from gevent import monkey;monkey.patch_all() from socket import * import gevent #如果不想用money.patch_all()打補丁,可以用gevent自帶的socket # from gevent import socket # s=socket.socket() def server(server_ip,port): s=socket(AF_INET,SOCK_STREAM) s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) s.bind((server_ip,port)) s.listen(5) while True: conn,addr=s.accept() gevent.spawn(talk,conn,addr) def talk(conn,addr): try: while True: res=conn.recv(1024) print('client %s:%s msg: %s' %(addr[0],addr[1],res)) conn.send(res.upper()) except Exception as e: print(e) finally: conn.close() if __name__ == '__main__': server('127.0.0.1',8080) 服務端 服務端
#_*_coding:utf-8_*_ __author__ = 'Linhaifeng' from socket import * client=socket(AF_INET,SOCK_STREAM) client.connect(('127.0.0.1',8080)) while True: msg=input('>>: ').strip() if not msg:continue client.send(msg.encode('utf-8')) msg=client.recv(1024) print(msg.decode('utf-8')) 客戶端
from threading import Thread from socket import * import threading def client(server_ip,port): c=socket(AF_INET,SOCK_STREAM) #套接字對象一定要加到函數內,即局部名稱空間內,放在函數外則被所有線程共享,則大家公用一個套接字對象,那么客戶端端口永遠一樣了 c.connect((server_ip,port)) count=0 while True: c.send(('%s say hello %s' %(threading.current_thread().getName(),count)).encode('utf-8')) msg=c.recv(1024) print(msg.decode('utf-8')) count+=1 if __name__ == '__main__': for i in range(500): t=Thread(target=client,args=('127.0.0.1',8080)) t.start() 多線程并發多個客戶端
asyncio
asyncio是Python 3.4版本引入的標準庫,直接內置了對異步IO的支持。
asyncio的編程模型就是一個消息循環。我們從asyncio模塊中直接獲取一個EventLoop的引用,然后把需要執行的協程扔到EventLoop中執行,就實現了異步IO。
注意asyncio在Python3.7中做出了改變,建議以后使用Python3.7以上的版本
有一個很好的博客
https://blog.csdn.net/qq_42658739/article/details/128437355
浙公網安備 33010602011771號