摘要:我們以請求網(wǎng)絡(luò)服務(wù)為例,來實(shí)際測試一下加入多線程之后的效果。所以,執(zhí)行密集型操作時(shí),多線程是有用的,對于密集型操作,則每次只能使用一個(gè)線程。說到這里,對于密集型,可以使用多線程或者多進(jìn)程來提高效率。
為了提高系統(tǒng)密集型運(yùn)算的效率,我們常常會使用到多個(gè)進(jìn)程或者是多個(gè)線程,python中的Threading包實(shí)現(xiàn)了線程,multiprocessing 包則實(shí)現(xiàn)了多進(jìn)程。而在3.2版本的python中,將進(jìn)程與線程進(jìn)一步封裝成concurrent.futures 這個(gè)包,使用起來更加方便。我們以請求網(wǎng)絡(luò)服務(wù)為例,來實(shí)際測試一下加入多線程之后的效果。
首先來看看不使用多線程花費(fèi)的時(shí)間:
import time import requests NUMBERS = range(12) URL = "http://httpbin.org/get?a={}" # 獲取網(wǎng)絡(luò)請求結(jié)果 def fetch(a): r = requests.get(URL.format(a)) return r.json()["args"]["a"] # 開始時(shí)間 start = time.time() for num in NUMBERS: result = fetch(num) print("fetch({}) = {}".format(num, result)) # 計(jì)算花費(fèi)的時(shí)間 print("cost time: {}".format(time.time() - start))
執(zhí)行結(jié)果如下:
fetch(0) = 0 fetch(1) = 1 fetch(2) = 2 fetch(3) = 3 fetch(4) = 4 fetch(5) = 5 fetch(6) = 6 fetch(7) = 7 fetch(8) = 8 fetch(9) = 9 fetch(10) = 10 fetch(11) = 11 cost time: 6.952988862991333
再來看看加入多線程之后的效果:
import time import requests from concurrent.futures import ThreadPoolExecutor NUMBERS = range(12) URL = "http://httpbin.org/get?a={}" def fetch(a): r = requests.get(URL.format(a)) return r.json()["args"]["a"] start = time.time() # 使用線程池(使用5個(gè)線程) with ThreadPoolExecutor(max_workers=5) as executor: # 此處的map操作與原生的map函數(shù)功能一樣 for num, result in zip(NUMBERS, executor.map(fetch, NUMBERS)): print("fetch({}) = {}".format(num, result)) print("cost time: {}".format(time.time() - start))
執(zhí)行結(jié)果如下:
fetch(0) = 0 fetch(1) = 1 fetch(2) = 2 fetch(3) = 3 fetch(4) = 4 fetch(5) = 5 fetch(6) = 6 fetch(7) = 7 fetch(8) = 8 fetch(9) = 9 fetch(10) = 10 fetch(11) = 11 cost time: 1.9467740058898926
只用了近2秒的時(shí)間,如果再多加幾個(gè)線程時(shí)間會更短,而不加入多線程需要接近7秒的時(shí)間。
不是說python中由于全局解釋鎖的存在,每次只能執(zhí)行一個(gè)線程嗎,為什么上面使用多線程還快一些?
確實(shí),由于python的解釋器(只有cpython解釋器中存在這個(gè)問題)本身不是線程安全的,所以存在著全局解釋鎖,也就是我們經(jīng)常聽到的GIL,導(dǎo)致一次只能使用一個(gè)線程來執(zhí)行Python的字節(jié)碼。但是對于上面的I/O操作來說,一個(gè)線程在等待網(wǎng)絡(luò)響應(yīng)時(shí),執(zhí)行I/O操作的函數(shù)會釋放GIL,然后再運(yùn)行一個(gè)線程。
所以,執(zhí)行I/O密集型操作時(shí),多線程是有用的,對于CPU密集型操作,則每次只能使用一個(gè)線程。那這樣說來,想執(zhí)行CPU密集型操作怎么辦?
答案是使用多進(jìn)程,使用concurrent.futures包中的ProcessPoolExecutor 。這個(gè)模塊實(shí)現(xiàn)的是真正的并行計(jì)算,因?yàn)樗褂肞rocessPoolExecutor 類把工作分配給多個(gè) Python 進(jìn)程處理。因此,如果需要做 CPU密集型處理,使用這個(gè)模塊能繞開 GIL,利用所有可用的 CPU 核心。
說到這里,對于I/O密集型,可以使用多線程或者多進(jìn)程來提高效率。我們上面的并發(fā)請求數(shù)只有5個(gè),但是如果同時(shí)有1萬個(gè)并發(fā)操作,像淘寶這類的網(wǎng)站同時(shí)并發(fā)請求數(shù)可以達(dá)到千萬級以上,服務(wù)器每次為一個(gè)請求開一個(gè)線程,還要進(jìn)行上下文切換,這樣的開銷會很大,服務(wù)器壓根承受不住。一個(gè)解決辦法是采用分布式,大公司有錢有力,能買很多的服務(wù)器,小公司呢。
我們知道系統(tǒng)開進(jìn)程的個(gè)數(shù)是有限的,線程的出現(xiàn)就是為了解決這個(gè)問題,于是在進(jìn)程之下又分出多個(gè)線程。所以有人就提出了能不能用同一線程來同時(shí)處理若干連接,再往下分一級。于是協(xié)程就出現(xiàn)了。
協(xié)程在實(shí)現(xiàn)上試圖用一組少量的線程來實(shí)現(xiàn)多個(gè)任務(wù),一旦某個(gè)任務(wù)阻塞,則可能用同一線程繼續(xù)運(yùn)行其他任務(wù),避免大量上下文的切換,而且,各個(gè)協(xié)程之間的切換,往往是用戶通過代碼來顯式指定的,不需要系統(tǒng)參與,可以很方便的實(shí)現(xiàn)異步。
協(xié)程本質(zhì)上是異步非阻塞技術(shù),它是將事件回調(diào)進(jìn)行了包裝,讓程序員看不到里面的事件循環(huán)。說到這里,什么是異步非阻塞?同步異步,阻塞,非阻塞有什么區(qū)別?
借用知乎上的一個(gè)例子,假如你打電話問書店老板有沒有《分布式系統(tǒng)》這本書,如果是同步通信機(jī)制,書店老板會說,你稍等,”我查一下",然后開始查啊查,等查好了(可能是5秒,也可能是一天)告訴你結(jié)果(返回結(jié)果)。而異步通信機(jī)制,書店老板直接告訴你我查一下啊,查好了打電話給你,然后直接掛電話了(不返回結(jié)果)。然后查好了,他會主動(dòng)打電話給你。在這里老板通過“回電”這種方式來回調(diào)。
而阻塞與非阻塞則是你打電話問書店老板有沒有《分布式系統(tǒng)》這本書,你如果是阻塞式調(diào)用,你會一直把自己“掛起”,直到得到這本書有沒有的結(jié)果,如果是非阻塞式調(diào)用,你不管老板有沒有告訴你,你自己先一邊去玩了, 當(dāng)然你也要偶爾過幾分鐘check一下老板有沒有返回結(jié)果。在這里阻塞與非阻塞與是否同步異步無關(guān)。跟老板通過什么方式回答你結(jié)果無關(guān)。
總之一句話,阻塞和非阻塞,描述的是一種狀態(tài),而同步與非同步描述的是行為方式。
回到協(xié)程上。
類似于Threading 包是對線程的實(shí)現(xiàn)一樣,python3.4之后加入的asyncio 包則是對協(xié)程的實(shí)現(xiàn)。我們用asyncio改寫文章開頭的代碼,看看使用協(xié)程之后能花費(fèi)多少時(shí)間。
import asyncio import aiohttp import time NUMBERS = range(12) URL = "http://httpbin.org/get?a={}" # 這里的代碼不理解沒關(guān)系 # 主要是為了證明協(xié)程的強(qiáng)大 async def fetch_async(a): async with aiohttp.request("GET", URL.format(a)) as r: data = await r.json() return data["args"]["a"] start = time.time() loop = asyncio.get_event_loop() tasks = [fetch_async(num) for num in NUMBERS] results = loop.run_until_complete(asyncio.gather(*tasks)) for num, results in zip(NUMBERS, results): print("fetch({}) = ()".format(num, results)) print("cost time: {}".format(time.time() - start))
執(zhí)行結(jié)果:
fetch(0) = () fetch(1) = () fetch(2) = () fetch(3) = () fetch(4) = () fetch(5) = () fetch(6) = () fetch(7) = () fetch(8) = () fetch(9) = () fetch(10) = () fetch(11) = () cost time: 0.8582110404968262
不到一秒!感受到協(xié)程的威力了吧。
asyncio的知識說實(shí)在的有點(diǎn)難懂,因?yàn)樗怯卯惒降姆绞皆诰帉懘a。上面給出的asyncio示例不理解也沒有關(guān)系,之后的文章會詳細(xì)的介紹一些asyncio相關(guān)的概念。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/41225.html
Python裝飾器為什么難理解? 無論項(xiàng)目中還是面試都離不開裝飾器話題,裝飾器的強(qiáng)大在于它能夠在不修改原有業(yè)務(wù)邏輯的情況下對代碼進(jìn)行擴(kuò)展,權(quán)限校驗(yàn)、用戶認(rèn)證、日志記錄、性能測試、事務(wù)處理、緩存等都是裝飾器的絕佳應(yīng)用場景,它能夠最大程度地對代碼進(jìn)行復(fù)用。 但為什么初學(xué)者對裝飾器的理解如此困難,我認(rèn)為本質(zhì)上是對Py… Python 實(shí)現(xiàn)車牌定位及分割 作者用 Python 實(shí)現(xiàn)車牌定位及分割的實(shí)踐。 ...
摘要:文章結(jié)構(gòu)來自七周七并發(fā)模型互斥和內(nèi)存模型創(chuàng)建線程這段代碼創(chuàng)建并啟動(dòng)了一個(gè)實(shí)例,首先從開始,函數(shù)的余下部分一起并發(fā)執(zhí)行。在鎖定狀態(tài)下,某些線程擁有鎖在非鎖定狀態(tài)下,沒有線程擁有它。 并發(fā)&并行 并發(fā)程序含有多個(gè)邏輯上的獨(dú)立執(zhí)行塊,他們可以獨(dú)立的并行執(zhí)行,也可以串行執(zhí)行。并行程序解決問題的速度比串行程序快的多,因?yàn)槠淇梢酝瑫r(shí)執(zhí)行整個(gè)任務(wù)的多個(gè)部分。并行程序可能有多個(gè)獨(dú)立執(zhí)行塊,也可能只有一...
摘要:開頭正式開啟我入職的里程,現(xiàn)在已是工作了一個(gè)星期了,這個(gè)星期算是我入職的過渡期,算是知道了學(xué)校生活和工作的差距了,總之,盡快習(xí)慣這種生活吧。當(dāng)時(shí)是看的廖雪峰的博客自己也用做爬蟲寫過幾篇博客,不過有些是在前人的基礎(chǔ)上寫的。 showImg(https://segmentfault.com/img/remote/1460000010867984); 開頭 2017.08.21 正式開啟我...
閱讀 2032·2023-04-26 00:59
閱讀 3288·2021-11-15 18:10
閱讀 3246·2021-09-22 16:02
閱讀 917·2021-09-02 15:15
閱讀 3892·2019-08-30 15:56
閱讀 2056·2019-08-30 15:54
閱讀 3008·2019-08-29 16:31
閱讀 2208·2019-08-29 16:10