python 基礎 day18
協程
協程,又稱微線程,纖程。
協程其實可以認爲是比線程更小的執行單元 ,爲啥說他是一個執行單元,因爲他自帶cpu上下文,這樣只要在合適的時機,我們可以把一個協程切換到另一個協程,只要這個過程中保存或者恢復cpu上下文,那麼這個程序還是可以運行的。
通俗的理解:在一個線程中的某個函數,可以在任何地方保存當前函數的一些臨時變量等信息,然後切換到另一個函數中執行,注意不是通過調用函數的方式做到的,並且切換的次數以及什麼時候再切換到原來的函數中都是由開發者自己確定的。
協程與線程的差異
協程的特點在一個線程內部執行,與多線程相比,協程有什麼優點?
協程的優點
-
最大的優勢就是協程有極高的執行效率,因爲子程序切換不是線程切換而是程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程的數量越多,協程的性能就會越明顯
-
第二大優勢就是不需要多線程的鎖機制,因爲只有一個線程,也不存在同時寫變量衝突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多。
因爲協程是一個線程執行,那麼怎麼利用多核cpu呢?
最簡單的方式就是多進程+協程,既充分利用多核,又能充分發揮協程的高效性,可獲得極高的性能。
協程的缺點
它不能同時將cpu的多核用上,只能使用一個核
python對協程的支持是通過generator實現的
在generator中,我們不但能夠通過for循環作用,我們還可以不斷調用next()函數,獲取由yield語句返回的下一個值
yield關鍵字的作用:
能過多次進入、多次返回,能夠暫停函數體中代碼的執行
在python的函數(function)定義中,只要出現了yield表達式(Yield expression),那麼事實上定義的是一個generator function, 調用這個generator function返回值是一個generator
def func():
yield 1
yield 2
if __name__ == '__main__':
g = func()
print(next(g))
print(next(g))
print(next(g)) #報錯StopIteration
協程簡單的實現
協程的實現原理【與yield的方式類似】
import time
def func():
while True:
print("====func===")
yield
time.sleep(1)
def func2(func):
while True:
print("====func2====")
next(func)
time.sleep(1)
if __name__ == '__main__':
#返回了一個生成器對象
f = func()
func2(f)
使用greenlet來實現協程
from greenlet import greenlet
import time
def fun1():
print("協程1...")
time.sleep(3)
g2.switch() #切換到協程g2
print("節日快樂。。。")
def fun2():
print("協程2...")
time.sleep(3)
g1.switch() #切換到協程g1
if __name__ == '__main__':
g1 = greenlet(fun1)
g2 = greenlet(fun2)
g1.switch() #切換到協程1
使用gevent實現
'''
使用gevent + sleep自動將CPU執行權分配給當前未睡眠的協程
'''
import gevent
def func1():
gevent.sleep(1)
print("人到中年不得已,保溫杯裏泡枸杞")
gevent.sleep(13)
print("1:over")
def func2():
gevent.sleep(3)
print("枸杞難擋歲月催,還得往裏加當歸")
gevent.sleep(9)
print("2:over")
def func3():
gevent.sleep(5)
print("當歸難擋歲月刀,人蔘鹿茸泡小燒")
gevent.sleep(5)
print("3:over")
def func4():
gevent.sleep(7)
print("人蔘鹿茸不管飽,還得腰子加腎寶")
gevent.sleep(1)
print("4:over")
def simpleGevent():
gr1 = gevent.spawn(func1)
gr2 = gevent.spawn(func2)
gr3 = gevent.spawn(func3)
gr4 = gevent.spawn(func4)
gevent.joinall([gr1, gr2, gr3, gr4])
if __name__ == '__main__':
simpleGevent()
使用gevent與monkey實現協程
from gevent import monkey#導入猴子補丁
monkey.patch_all() #可以實現協程的自動切換
import requests
import gevent
import threading
import time
headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36'}
def get_data(url):
print("協程",url)
response = requests.get(url,headers=headers)
print(url,"response",response.text)
time.sleep(3)
if __name__ == '__main__':
#協程爬取數據
t1 = time.time()
url_list = [
'http://www.baidu.com',
'http://www.qq.com',
'http://www.ifeng.com',
'http://www.sina.com.cn',
'http://www.taobao.com',
]
g_list = []
for url in url_list:
g = gevent.spawn(get_data,url)
g_list.append(g)
gevent.joinall(g_list)
#3.4110121726989746
#多線程爬取數據
# t_list = []
# for url in url_list:
# t = threading.Thread(target=get_data,args=(url,))
# t.start()
# t_list.append(t)
# for t in t_list:
# t.join()
print(time.time()-t1)
#4.9332239627838135
協程爬取鏈家
import gevent
from gevent import monkey
monkey.patch_all()
import threading
import time
import requests
# 模擬瀏覽器
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36"
}
def get_page(page):
url = 'https://sz.lianjia.com/ershoufang/pg%d/' % page
response = requests.get(url, headers=headers)
print('第%d頁' % page, len(response.text))
# print('第%d頁' % page)
if __name__ == '__main__':
start = time.time()
g_list=[]
for i in range(1,200):
g = gevent.spawn(get_page,i)
g_list.append(g)
#執行
gevent.joinall(g_list)
#6.445696830749512
# t_list = []
# for i in range(1,200):
# t = threading.Thread(target=get_page,args=(i,))
# t.start()
# t_list.append(t)
#
# for t in t_list:
# t.join()
print(threading.current_thread().name)
end = time.time()
print(end-start)
#5.983382940292358
進程
進程是程序的一次性執行的過程,正在進行的一個過程或者任務,而負責執行任務的是cpu。
進程的生命週期:
當操作系統要完成某個任務的時候它會創建一個進程,當進程完成任務之後,系統就會撤銷這個進程,收回它所佔用的資源,從創建到撤銷的時間段就是進程的生命週期。
進程之間存在併發:
在一個操作系統中,同時會存在多個進程,它們就輪流佔用cpu資源。
並行與併發的區別:
無論並行還是併發,在用戶看來其實都是同時運行的,不管是進程還是線程,都只是一個任務而已,真正幹活的是cpu,而一個cpu(單核)同一時刻只能執行一個任務。
並行:多個任務同時運行,只有具備多個cpu才能真正的實現並行,含有幾個cpu,也就意味着同一時間可以執行幾個任務。
併發:是僞並行,看起來是同時運行的,實際上是單個cpu在多道程序之間的來回切換。
同步與異步的概念:
同步就是指一個進程在執某個請求的時候,若該請求需要一段時間才能返回信息,那麼這個進程將會一直等待下去,直到返回信息才能繼續執行下去。
異步:是指進程不需要一直等待下去,而是繼續執行下面的操作,不管其他進程的狀態,當有消息返回的時候,系統會通知進程來進行處理,這樣可以提高執行效率。
比如:打電話的過程就是同步通訊,發短信就是異步通訊
多線程和多進程的關係:
對於計算機密集型應用,應該使用多進程
對於IO密集型應用,應該使用多線程,線程的創建比進程的創建開銷小的多。
創建進程
import multiprocessing
import time
from time import sleep
def func(*args):
sleep(3)
print("子進程",multiprocessing.current_process().name)
print(args)
if __name__ == '__main__':
t = time.time()
p_list = []
for x in range(300):
pro = multiprocessing.Process(target=func,args=("hello",))
pro.start()
p_list.append(pro)
for p in p_list:
p.join()
print(time.time()-t)
進程鎖與信號量
import multiprocessing
import time
import random
def func2(i,lock):
with lock:
print("加鎖",i)
time.sleep(1)
print("釋放鎖",i)
#信號量,控制進程的最大併發數
def func(num,sem):
with sem:
print("子進程", num)
time.sleep(time.time())
print("結束",num)
if __name__ == '__main__':
sem = multiprocessing.Semaphore(4)
lock = multiprocessing.Lock()
start = time.time()
p_list = []
for x in range(1,30):
p = multiprocessing.Process(target=func2,args=(x,lock))
p.start()
p_list.append(p)
for p in p_list:
p.join()
print(time.time()-start)
多進程加協程分頁爬取鏈家
import gevent
import time
import requests
import re
import multiprocessing
# 模擬瀏覽器
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36"
}
#獲取所有的區與鏈接
def get_all_area():
url = 'https://sz.lianjia.com/ershoufang/pg1/'
response = requests.get(url,headers=headers)
content = response.text
pattern = '<div data-role="ershoufang" >(.*?)</div>'
area_str = re.findall(pattern, content, re.S)[0]
# print(area_str)
url_pattern = 'href="(.*?)"'
url_list = re.findall(url_pattern, area_str, re.S)
# print(url_list)
areaname_pattern = '<a(?:.*?)>(.*?)</a>'
areaname_list = re.findall(areaname_pattern, area_str, re.S)
# print(areaname_list)
return url_list, areaname_list
#獲取每一頁的內容
def get_page(page,url,area):
url ='%s/pg%d/'%(url,page)
response = requests.get(url,headers=headers)
print('{}:第{}頁'.format(area,page))
#分區爬取數據使用協程
def get_area(url,area):
g_list = []
for i in range(1,101):
g = gevent.spawn(get_page,i,url,area)
g_list.append(g)
gevent.joinall(g_list)
if __name__ == '__main__':
#獲取所有區
url_list,area_list = get_all_area()
t = time.time()
p_list = []
for i in range(len(url_list)):
url = 'https://sz.lianjia.com' + url_list[i]
area = area_list[i]
p = multiprocessing.Process(target=get_area,args=(url,area))
p.start()
p_list.append(p)
for p in p_list:
p.join()
e = time.time()
print(t-e)