對python併發編程的思考

爲了提升系統密集型運算的效率,咱們經常會使用到多個進程或者是多個線程,python中的Threading包實現了線程,multiprocessing 包則實現了多進程。而在3.2版本的python中,將進程與線程進一步封裝成concurrent.futures 這個包,使用起來更加方便。咱們以請求網絡服務爲例,來實際測試一下加入多線程以後的效果。python

首先來看看不使用多線程花費的時間:程序員

import time
import requests

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()

for num in NUMBERS:
    result = fetch(num)
    print('fetch({}) = {}'.format(num, result))
# 計算花費的時間
print('cost time: {}'.format(time.time() - start))

執行結果以下:json

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個線程)
with ThreadPoolExecutor(max_workers=5) as executor:
  # 此處的map操做與原生的map函數功能同樣
    for num, result in zip(NUMBERS, executor.map(fetch, NUMBERS)):
        print('fetch({}) = {}'.format(num, result))
print('cost time: {}'.format(time.time() - start))

執行結果以下:服務器

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秒的時間,若是再多加幾個線程時間會更短,而不加入多線程須要接近7秒的時間。網絡

不是說python中因爲全局解釋鎖的存在,每次只能執行一個線程嗎,爲何上面使用多線程還快一些?多線程

確實,因爲python的解釋器(只有cpython解釋器中存在這個問題)自己不是線程安全的,因此存在着全局解釋鎖,也就是咱們常常聽到的GIL,致使一次只能使用一個線程來執行Python的字節碼。可是對於上面的I/O操做來講,一個線程在等待網絡響應時,執行I/O操做的函數會釋放GIL,而後再運行一個線程。併發

因此,執行I/O密集型操做時,多線程是有用的,對於CPU密集型操做,則每次只能使用一個線程。那這樣說來,想執行CPU密集型操做怎麼辦?異步

答案是使用多進程,使用concurrent.futures包中的ProcessPoolExecutor 。這個模塊實現的是真正的並行計算,由於它使用ProcessPoolExecutor 類把工做分配給多個 Python 進程處理。所以,若是須要作 CPU密集型處理,使用這個模塊能繞開 GIL,利用全部可用的 CPU 核心。async

說到這裏,對於I/O密集型,可使用多線程或者多進程來提升效率。咱們上面的併發請求數只有5個,可是若是同時有1萬個併發操做,像淘寶這類的網站同時併發請求數能夠達到千萬級以上,服務器每次爲一個請求開一個線程,還要進行上下文切換,這樣的開銷會很大,服務器壓根承受不住。一個解決辦法是採用分佈式,大公司有錢有力,能買不少的服務器,小公司呢。

咱們知道系統開進程的個數是有限的,線程的出現就是爲了解決這個問題,因而在進程之下又分出多個線程。因此有人就提出了能不能用同一線程來同時處理若干鏈接,再往下分一級。因而協程就出現了。

協程在實現上試圖用一組少許的線程來實現多個任務,一旦某個任務阻塞,則可能用同一線程繼續運行其餘任務,避免大量上下文的切換,並且,各個協程之間的切換,每每是用戶經過代碼來顯式指定的,不須要系統參與,能夠很方便的實現異步。

協程本質上是異步非阻塞技術,它是將事件回調進行了包裝,讓程序員看不到裏面的事件循環。說到這裏,什麼是異步非阻塞?同步異步,阻塞,非阻塞有什麼區別?

借用知乎上的一個例子,假如你打電話問書店老闆有沒有《分佈式系統》這本書,若是是同步通訊機制,書店老闆會說,你稍等,」我查一下",而後開始查啊查,等查好了(多是5秒,也多是一天)告訴你結果(返回結果)。而異步通訊機制,書店老闆直接告訴你我查一下啊,查好了打電話給你,而後直接掛電話了(不返回結果)。而後查好了,他會主動打電話給你。在這裏老闆經過「回電」這種方式來回調。

而阻塞與非阻塞則是你打電話問書店老闆有沒有《分佈式系統》這本書,你若是是阻塞式調用,你會一直把本身「掛起」,直到獲得這本書有沒有的結果,若是是非阻塞式調用,你無論老闆有沒有告訴你,你本身先一邊去玩了, 固然你也要偶爾過幾分鐘check一下老闆有沒有返回結果。在這裏阻塞與非阻塞與是否同步異步無關。跟老闆經過什麼方式回答你結果無關。

總之一句話,阻塞和非阻塞,描述的是一種狀態,而同步與非同步描述的是行爲方式。

回到協程上。

相似於Threading 包是對線程的實現同樣,python3.4以後加入的asyncio 包則是對協程的實現。咱們用asyncio改寫文章開頭的代碼,看看使用協程以後能花費多少時間。

import asyncio
import aiohttp
import time

NUMBERS = range(12)
URL = 'http://httpbin.org/get?a={}'
# 這裏的代碼不理解不要緊
# 主要是爲了證實協程的強大
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))

執行結果:

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

不到一秒!感覺到協程的威力了吧。

asyncio的知識說實在的有點難懂,由於它是用異步的方式在編寫代碼。上面給出的asyncio示例不理解也沒有關係,以後的文章會詳細的介紹一些asyncio相關的概念。

相關文章
相關標籤/搜索