Python並發編程初步

2021-01-07 蟲蟲搜奇

我們知道現在硬體飛速發展,多核CPU 成了標配。為了提高程序的效率,一個方面改變程序的順序執行,用異步方式,防止由於某個耗時步驟,而影響後續程序的執行。另一個方面是採用並發方式執行,重複利用多核CPU優勢加速執行。關於並發編程大家可能比較熟悉的是Golang的協程、通道和Node.js 的async.parallel異步並發編程。就並發編程來說,Python不是一門合適的語言,主要是Python有一個解析器(CPython)內置的全局解釋鎖GIL。 GIL限制Python中一次只能有一個線程訪問Python對象,從而我們無法實現多線程分配到多個CPU執行,這是一個極大限制,限制Python並發編程。當然限制歸限制,Python標準庫中都已經引入了多進程和多線程庫,所以Python並發程序相當簡單。

本文中,蟲蟲給大家實例介紹一下Python的並發編程

並發編程

關於python並發編程,我們推薦優雅地創建並發程序三部曲:

首先,編寫一個按順序執行任務的腳本。

其次,腳本中的執行程序(耗時任務)提取為一個執行函數,並使用map函數調用。

最後,使用並發模塊中的函數替換map即可。

實例腳本

該實例中,我們用到一個小的圖片爬蟲,使用urllib從Picsum下載20張圖片,具體腳本程序如下:

pic_get.py

import urllib.request

import time

url = 'picsum.photos/id/{}/200/300'

args = [(n, url.format(n)) for n in range(20)]

start = time.time()

for pic_id, url in args:

res = urllib.request.urlopen(url)

pic = res.read()

with open(f'./{pic_id}.jpg', 'wb') as f:

f.write(pic)

print(f'圖片 {pic_id} 已經保存!')

end = time.time()

msg ='共耗時 {:.3f} 秒下載完成。'

print(msg.format(end-start))

python pic_get.py 運行該腳本,結果如下:

圖片 0 已經保存!

圖片 1 已經保存!

圖片 2 已經保存!

...

共耗時 26.694 秒下載完成。

下載共耗費不到半分鐘,接著按照我們優雅的三部曲,改造這個腳本。

使用Map改造腳本

下面腳本中,我們將下載圖片的代碼打包到一個執行函數get_img中。

# pic_get1.py

import urllib.request

import time

def get_img(pic_id, url):

res = urllib.request.urlopen(url)

pic = res.read()

with open(f'test/{pic_id}.jpg', 'wb') as f:

f.write(pic)

print(f'圖片 {pic_id} 已經保存!')

def main():

url = 'picsum.photos/id/{}/200/300'

pic_ids = [i for i in range(20)] ;

urls=[(url.format(n)) for n in range(20)]

start = time.time()

for _ in map(get_img, pic_ids, urls):

pass

end = time.time()

msg = '共耗時{:.3f}秒下載完成。'

print(msg.format(end-start))

if __name__ == '__main__':

main()

上述腳本中,用map函數替換先前腳本中的for循環(黑體部分)。map是一個函數式編程語法,該函數會生成一個迭代器,迭代器會執行迭代調用get_img()。關於map()函數熟悉函數式編程人可能會覺得有點奇怪,請自己搜索資料充電,此處,我們用它來充當並發編程網關。

圖片 0 已經保存!

圖片 1 已經保存!

圖片 2 已經保存!

...

圖片 19 已經保存!

共耗時26.023秒下載完成。

用map改造後,運行腳本總耗時大體上和腳本一致。

多線程並發處理

Python標準庫的current.futures模塊包含了大量並發編程的包裝函數,詳細說明,可參見官方文檔,此處我們直接上代碼。

將pic_get1.py中的程序做簡單改進,就能實現多線程腳本:

首先在腳本開頭引入多線程函數:

from concurrent.futures import ThreadPoolExecutor

接著替換

for _ in map(download_img, pic_ids, urls):

pass

with ThreadPoolExecutor(max_workers=20) as do:

do.map(get_img, pic_ids, urls)

即可。執行

圖片 0 已經保存!

圖片 2 已經保存!

圖片 5 已經保存!

...

圖片 9 已經保存!

共耗時2.913秒下載完成。

總耗時由26秒,減少到了大約3秒。大概快了8倍。並發執行的效果還是槓槓的。

程序中我們使用with ThreadPoolExecutor語句產生一個執行器do。通過將get_img和相應的參數映射到執行程序,自動生成多線程執行。

大家可能注意到了在多線程腳本執行後,圖片下載時候不是以前的0~19的順序的,而是不同線程並發執行的所以完成提示信息也是亂序的。

多進程處理

多進程的改造也非常簡單,我麼只需把之前多線程腳本中的ThreadPoolExecutor替換為ProcessPoolExecutor即可。

from concurrent.futures import ProcessPoolExecutor

with ProcessPoolExecutor(max_workers=20) as do:

do.map(get_img, pic_ids, urls)

執行結果:

圖片 9 已經保存!

圖片 6 已經保存!

...

圖片 11 已經保存!

圖片 15 已經保存!

共耗時4.606秒下載完成

也非常快了,4秒鐘就完成了,但是比多線程的3秒,稍微慢點。為什麼多進程要比多線程慢呢?顧名思義,多進程程序會啟用多個進程,而多線程會使用線程。Python中一個進程可以運行多個線程。每個進程都有其適當的Python解釋器和適當的GIL。相比較而已,啟動一個進程是更加耗時,重的操作,所以需要花費的時間更多。

斐波那契數列計算

為了進一步說明Python中線程和進程之間的區別,我們再來舉一個大量計算的例子,斐波那契數列的計算。

根據斐波那契數列的定義我們用遞歸方法編寫實現其計算:

def fib(n):

if n == 1:

return 0

elif n == 2:

return 1

else:

return fib(n-1) + fib(n-2)

在不使用numpy的情況下用普通Python計算比較慢:

def main():

fib_range = list(range(1, 35))

times = []

for run in range(10):

start = time.time()

for n in fib_range:

fib(n)

end = time.time()

times.append(end-start)

print('波那契數列fib(35)計算平均耗時 {:.3f}。'.format(np.mean(times)))

波那契數列fib(35)計算平均耗時 5.200

下面我們用並發計算來加速計算。

讓我們通過線程加速它!為此,我用受信任的ThreadPoolExecutor替換for循環,如下所示:

with ThreadPoolExecutor() as do:

do.map(fib, fib_range)

執行結果:

波那契數列fib(35)計算平均耗時 5.239。

什麼?加速後,反而慢,好像多線程沒起到作用。這就是GIL的因素導致的,儘管使用了多個線程,生成了一堆線程,但是這些線程都在同一進程中運行並共享一個GIL。所以斐波那契序列儘管是並發計算的,這些線程在只能在一個CPU上循序執行。

進程可以分布在不同的CPU核心,而在同一進程上運行的線程則不能。使CPU消耗最大的操作為CPU綁定操作。為了加快CPU限制的操作,應該啟動多個進程計算。我們用ProcessPoolExecutor替換ThreadPoolExecutor再試試:

波那契數列fib(35)計算平均耗時 3.591

性能提高了一點。

除了並發的方式外,我們可以用算法優化方法來提高性能,在數值計算中,這是一種更有效的方法,比如,我們改造fib函數:

def fib(n):

a, b, i = 0, 1, 1

while i < n:

a, b = b, a + b

i += 1

return b

上述方法中,巧妙用內存存中的變量歷史迭代的前兩次結果都存在內存中,所以該次計算中無需回溯迭代計算,這樣計算效率O(1),基本上可以秒出結果。

使用新算法後的執行結果:

波那契數列fib(35)計算平均耗時 0.000。

總結

本文我們實例介紹了Python中的並發編程,關於並發編程由於標準庫中給我們打包好了方便使用的並發函數使得其使用非常方便。需要注意的是Python中的並發不管是多線程在IO操作中是有效的,而在其他方面,如數值結算時候就受GIL限制無用了。關於並發計算和GIL有心的話,可以參考有關文檔進一步深入學習了解。

相關焦點

  • 揭秘Python並發編程——協程
    Python並發編程一直是進階當中不可跨越的一道坎,其中包括進程、線程、協程,今天我們就來聊一聊協程。希望大家這個例子可以幫助大家更好的運用python。
  • 多任務並發編程需要學習的內容有哪些?
    Python多任務並發編程需要學習的內容有哪些?並發編程的目的是為了讓程序運行得更快,分工,高效地拆解任務並分配給線程;同步,線程之間如何協作; 互斥,保證同一時刻只允許一個線程訪問共享資源。需要學習多線程、多進程的創建,互斥鎖,死鎖,集全局變量等問題的解決方案。
  • Python 3.8異步並發編程
    有效的提高程序執行效率的兩種方法是異步和並發,Golang,node.js之所以可以有很高執行效率主要是他們的協程和異步並發機制。實際上異步和並發是每一種現代語言都在追求的特性,當然Python也不例外,今天我們就講講Python 3中的異步並發編程。
  • 並發進行時--python編程必知必會(3)
    :",pt2-pt1 at1=time.time() map(do,aimlist) at2=time.time() print u"單進程的執行時間是:",at2-at1並發的執行時間是:2.64899992943單進程的執行時間是:10.4660000801從結果可見,單一進程的執行時間剛好是多進程的
  • 尹立博:Python 全局解釋器鎖與並發 | AI 研習社第 59 期猿桌會
    ,Python 是一種多用途、高級別、面向對象、交互式、解釋型和對用戶非常友好的程式語言,擁有卓越的可讀性和極高的自由度。>雷鋒網 AI 研習社將其分享內容整理如下:今天要跟大家分享的是 Python 全局解釋器鎖與並發。
  • Python遊戲編程
    Python遊戲編程課程目標本課程把遊戲開發實踐應用於python編程課程的教學之中,培養學生對編程的興趣
  • 理解Python並發編程一篇就夠了PoolExecutor篇
    看一下花費的時間:❯ python fib_executor.pyfib(25) = 75025fib(26) = 121393fib(27) = 196418fib(28) = 317811fib(29) = 514229fib(30) = 832040fib(31) = 1346269
  • Python零基礎編程——起步並搭建環境
    二 Python 初步認識1-Python是什麼?Python是一門程式語言,不同的程序運行在不同的環境中。例如我們手機有安卓、蘋果,安卓的手機有用程序App用Java開發,蘋果的app程序用Swift。例如微信,安卓版的微信和蘋果版的微信是由不同的程式語言開發出來的。有同學會問為什麼不統一由一種語言開發到各個環境運行呢?這個是商業利益問題,不同的生態圈導致。
  • Python入門基礎之socket多線程編程,TCP伺服器和客戶端通信
    在上一篇文章中,我介紹了一下python3 socket編程的基礎,包括TCP、UDP客戶端和伺服器的建立過程(連結在最下方)。不過那個只是單線程的,伺服器一次只能和一個客戶端會話,多個客戶端的話只能等待。我們平時的應用中,伺服器肯定是要並發的,所以,今天將介紹socket編程的多線程編程。
  • Python視頻教程網課編程零基礎入門數據分析網絡爬蟲全套Python...
    6套課程) 資料庫操作(1套課程) python高級編程(6套課程) 注:零基礎全能篇中,針對windows和liunx系統均有兩套課程可供選擇學習,單純學python,哪個系統都一樣,初學者只需選擇自己熟悉的系統學習相應課程即可。
  • Scratch、Python學哪個?兒童編程怎麼學?全面解析編程(上)
    如果您的小孩準備學習編程或者已經在學編程,建議您認真看完該系列文章,一定會對孩子學習編程有所幫助。本系列文共分為三部分,第一部分講編程是什麼?編程到底學什麼?第二部分講為什麼要學習編程?學習編程,孩子能收穫什麼?第三部分講到底要怎麼學編程?有哪些需要注意的問題。
  • python編程入門,零基礎學習Python基礎教程
    這裡推薦這門python編程入門基礎教程,適合零基礎的同學學習!python軟體工程師都學什麼?自學Python,看視頻學的更快、更透徹一些,給你個課程大綱!階段二:Python高級編程和資料庫開發Python全棧開發與人工智慧之Python高級編程和資料庫開發知識學習內容包括:面向對象開發、Socket網絡編程、線程、進程、隊列、IO多路模型、Mysql資料庫開發等。
  • Python並發編程很簡單,一文搞清如何使用構造器創建線程
    上次已經和大家探討了關於進程和線程的區別和聯繫相關的東東,今天呢,咱們再次回到Python哦,和大家一起聊聊如何進行Python並發編程哦,大家也可以理解為如何在Python中進行多線程編程哦!好啦,廢話少說,咱們就開始吧!
  • 求職大視野 | 編程零基礎應當如何開始學習 Python?
    零基礎學編程,用python入門是個不錯的選擇,雖然國內基本上還是以c語言作為入門開發語言,但在國外
  • Python並發編程很簡單,一文幫你搞清如何創建線程類
    對於Python的並發編程相關的東東,相信通過上次咱們的探討,大家已經比較清楚了,對於Python創建線程的方式主要有兩種,這個上次咱們也已經說過了哦,第一種是使用threading模塊的Thread類的構造器來創建線程,這種方式上次咱們已經詳細討論過了哦,這次呢,咱們就重點和大家來聊聊第二種方式吧
  • 「原創」Java並發編程系列02|並發編程三大核心問題
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫寫在前面編寫並發程序是比較困難的,因為並發程序極易出現Bug,這些Bug有都是比較詭異的,很多都是沒辦法追蹤,而且難以復現。
  • 程式設計師學習編程,學習這四門程式語言就夠了
    中國程式設計師都有一個讓人難於理解的問題,特別是新手程式設計師,都有喜歡不斷學習最近熱門的程式語言,比如近一年的來的python超過java成為熱度排名第一,同時我們也發現很多程式設計師開始學習盲目ython,作為一名專業的程式設計師,沒有必要把程式語言都學完,比較目前程式語言有不少200種,每種程式語言都有成為熱度的可能性
  • 原創】Java並發編程系列01|開篇獲獎感言
    ,他剛工作時的並發編程第一原則就是不要寫並發程序。所以,並發編程已經成為一項必備技能。並發編程是Java語言的重要特性之一,它能使複雜的代碼變得更簡單,從而極大地簡化複雜系統的開發。並發編程可以充分發揮多處理器系統的強大計算能力,隨著處理器數量的持續增長,如何高效的並發變得越來越重要。
  • 徹底理解Java並發編程原理!
    Java並發編程為四大部分:計算機並發基礎知識、JDK內置並發框架、JDK並發包剖析以及其它並發知識。具體包括線程的狀態、Java線程調度策略、線程優先級、並發模型、悲觀鎖樂觀鎖、JDK各種同步器、JDK內置AQS同步器、線程與IO、Java線程與JVM線程、阻塞與喚醒機制、JDK並發包各種工具剖析、自旋、JDK內置並發鎖、CAS、synchronized、線程池、線程之間的協作等並發方面知識及原理進行深入淺出的講解。
  • 為什麼用 Java —— 關於並發編程
    和陶師兄每次吃飯聊天都覺得可以從他那學習到很多他多年在 Infrasturcture 上積累下來的寶貴經驗。本來這個話題其實想讓他自己寫的,可是貌似很多牛人對寫文章還是有點惰性的,所以今天很多的內容來自於陶濤師兄,不過還是由我操筆了。提高並發的兩種模式提高高並發其實有兩種大思路。