一、程序池與線程池
實作并發的手段有兩種,多線程和多程序。注:并發是指多個任務看起來是同時運作的。主要是切換+儲存狀态。
當我們需要執行的并發任務大于cpu的核數時,我們需要知道一個作業系統不能無限的開啟程序和線程,通常有幾個核就開幾個程序,如果程序開啟過多,就無法充分利用cpu多核的優勢,效率反而會下降。這個時候就引入了程序池線程池的概念。
池的功能就是限制啟動的程序數或線程數
concurent.future子產品:
concurrent.futures子產品提供了高度封裝的異步調用接口
ProcessPoolExecutor: 程序池,提供異步調用
p = ProcessPoolExecutor(max_works)對于程序池如果不寫max_works:預設的是cpu的數目,預設是4個
ThreadPoolExecutor:線程池,提供異步調用
p = ThreadPoolExecutor(max_works)對于線程池如果不寫max_works:預設的是cpu的數目*5
補充:
送出任務的兩種方式:
# 同步調用:送出完一個任務之後,就在原地等待,等待任務完完整整地運作完畢拿到結果後,再執行下一行代碼,會導緻任務是串行執行的
# 異步調用:送出完一個任務之後,不在原地等待,結果???,而是直接執行下一行代碼,會導緻任務是并發執行的
程序池從無到有建立程序後,然後會固定使用程序池裡建立好的程序去執行所有任務,不會開啟其他程序
# 基本方法
#submit(fn, *args, **kwargs)
異步送出任務
#map(func, *iterables, timeout=None, chunksize=1)
取代for循環submit的操作
#shutdown(wait=True)
相當于程序池的pool.close()+pool.join()操作
wait=True,等待池内所有任務執行完畢回收完資源後才繼續
wait=False,立即傳回,并不會等待池内的任務執行完畢
但不管wait參數為何值,整個程式都會等到所有任務執行完畢
submit和map必須在shutdown之前
#result(timeout=None)
取得結果
#add_done_callback(fn)
回調函數

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
import time,random,os
import requests
def get(url):
print('%s GET %s' %(os.getpid(),url))
time.sleep(3)
response=requests.get(url)
if response.status_code == 200:
res=response.text
else:
res='下載下傳失敗'
return res
def parse(future):
time.sleep(1)
res=future.result()
print('%s 解析結果為%s' %(os.getpid(),len(res)))
if __name__ == '__main__':
urls=[
'https://www.baidu.com',
'https://www.sina.com.cn',
'https://www.tmall.com',
'https://www.jd.com',
'https://www.python.org',
'https://www.openstack.org',
'https://www.baidu.com',
'https://www.baidu.com',
'https://www.baidu.com',
]
p=ProcessPoolExecutor(9)
start=time.time()
for url in urls:
future=p.submit(get,url)
# 異步調用:送出完一個任務之後,不在原地等待,而是直接執行下一行代碼,會導緻任務是并發執行的,,結果futrue對象會在任務運作完畢後自動傳給回調函數
future.add_done_callback(parse) #parse會在任務運作完畢後自動觸發,然後接收一個參數future對象
p.shutdown(wait=True)
print('主',time.time()-start)
print('主',os.getpid())
test
線程池與程序池相比 他們的同步執行和異步執行是一樣的:

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
from threading import current_thread
import time,random,os
import requests
def get(url):
print('%s GET %s' %(current_thread().name,url))
time.sleep(3)
response=requests.get(url)
if response.status_code == 200:
res=response.text
else:
res='下載下傳失敗'
return res
def parse(future):
time.sleep(1)
res=future.result()
print('%s 解析結果為%s' %(current_thread().name,len(res)))
if __name__ == '__main__':
urls=[
'https://www.baidu.com',
'https://www.sina.com.cn',
'https://www.tmall.com',
'https://www.jd.com',
'https://www.python.org',
'https://www.openstack.org',
'https://www.baidu.com',
'https://www.baidu.com',
'https://www.baidu.com',
]
p=ThreadPoolExecutor(4)
for url in urls:
future=p.submit(get,url)
future.add_done_callback(parse)
p.shutdown(wait=True)
print('主',current_thread().name)
map函數:

# 我們的那個p.submit(task,i)和map函數的原理類似。我們就
# 可以用map函數去代替。更減縮了代碼
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import os, time, random
def task(n):
print('[%s] is running' % os.getpid())
time.sleep(random.randint(1, 3)) # I/O密集型的,,一般用線程,用了程序耗時長
return n ** 2
if __name__ == '__main__':
p = ProcessPoolExecutor()
obj = p.map(task, range(10))
p.shutdown() # 相當于close和join方法
print('=' * 30)
print(obj) # 傳回的是一個疊代器
print(list(obj))
View Code
回調函數(知乎):https://www.zhihu.com/question/19801131/answer/27459821
二、協程
在單線程的情況下實作并發。
遇到IO就切換就可以降低單線程的IO時間,進而最大限度地提升單線程的效率。
實作并發是讓多個任務看起來同時運作(切換+儲存狀态),cpu在運作一個任務的時候,會在兩種情況下去執行其他的任務,一種是遇到了I/O操作,一種是計算時間過長。其中第二種情況使用線程并發并不能提升效率,運算密集型的并發反而會降低效率。

#串行執行
import time
def func1():
for i in range(10000000):
i+1
def func2():
for i in range(10000000):
i+1
start = time.time()
func1()
func2()
stop = time.time()
print(stop - start)#1.675490379333496
串行執行

#基于yield并發執行
import time
def func1():
while True:
print('func1')
100000+1
yield
def func2():
g=func1()
for i in range(10000000):
print('func2')
time.sleep(100)
i+1
next(g)
start=time.time()
func2()
stop=time.time()
print(stop-start)
基于yield并發執行
yield複習:
函數中隻有有yield,這個函數就變成了一個生成器,調用函數不會執行函數體代碼,會得到一個傳回值,傳回值就是生成器對象。

def yield_test(n):
for i in range(n):
yield call(i)
print("i=",i)
#做一些其它的事情
print("do something.")
print("end.")
def call(i):
return i*2
#使用for循環
for i in yield_test(5):
print(i,",")
協程的本質就是在單線程下,由使用者自己控制一個任務遇到IO操作就切換到另一個任務去執行,以此來提升效率。
Gevent:
gevent是第三方庫,通過greenlet實作協程,其基本思想是:
當一個greenlet遇到IO操作時,比如通路網絡,就自動切換到其他的greenlet,等到IO操作完成,再在适當的時候切換回來繼續執行。由于IO操作非常耗時,經常使程式處于等待狀态,有了gevent為我們自動切換協程,就保證總有greenlet在運作,而不是等待IO。
由于切換是在IO操作時自動完成,是以gevent需要修改Python自帶的一些标準庫,這一過程在啟動時通過monkey patch完成:
我們用等待的時間模拟IO阻塞 在gevent子產品裡面要用gevent.sleep(5)表示等待的時間 要是我們想用time.sleep(),就要在最上面導入from gevent import monkey;monkey.patch_all()這句話 如果不導入直接用time.sleep(),就實作不了單線程并發的效果了
注:猴子更新檔需要在第一行就運作

from gevent import monkey;monkey.patch_all()
from gevent import spawn,joinall #pip3 install gevent
import time
def play(name):
print('%s play 1' %name)
time.sleep(5)
print('%s play 2' %name)
def eat(name):
print('%s eat 1' %name)
time.sleep(3)
print('%s eat 2' %name)
start=time.time()
g1=spawn(play,'lxx')
g2=spawn(eat,'lxx')
# g1.join()
# g2.join()
joinall([g1,g2])
print('主',time.time()-start)
gevent.spawn()”方法會建立一個新的greenlet協程對象,并運作它。”gevent.joinall()”方法會等待所有傳入的greenlet協程運作結束後再退出,這個方法可以接受一個”timeout”參數來設定逾時時間,機關是秒。
在單線程内實作socket并發:

from gevent import monkey;monkey.patch_all()
from socket import *
from gevent import spawn
def comunicate(conn):
while True: # 通信循環
try:
data = conn.recv(1024)
if len(data) == 0: break
conn.send(data.upper())
except ConnectionResetError:
break
conn.close()
def server(ip, port, backlog=5):
server = socket(AF_INET, SOCK_STREAM)
server.bind((ip, port))
server.listen(backlog)
while True: # 連結循環
conn, client_addr = server.accept()
print(client_addr)
# 通信
spawn(comunicate,conn)
if __name__ == '__main__':
g1=spawn(server,'127.0.0.1',8080)
g1.join()
server

from threading import Thread,current_thread
from socket import *
def client():
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))
n=0
while True:
msg='%s say hello %s' %(current_thread().name,n)
n+=1
client.send(msg.encode('utf-8'))
data=client.recv(1024)
print(data.decode('utf-8'))
if __name__ == '__main__':
for i in range(500):
t=Thread(target=client)
t.start()
client
焚膏油以繼晷,恒兀兀以窮年。