天天看點

python多線程并發_[程式設計-Python-并發程式設計]-1

1. 基礎原理

1.1 overview
  • 并發? 并行? 串行? 同步? 異步? 阻塞? 程序? 線程? 協程?
  • python中的并發有哪些?
  • 各自的使用場景?
1.2 并發? 并行? 串行?
  • 單個處理器核心(一個單核CPU) 在某一個時刻隻能處理一個程序(線程), 任何語言都是這樣
  • 并發: 在一個 時間段 ,處理多個任務, 單核 也可以并發 (CPU分時間片), 是以存在多個任務競争cpu單核心,存在任務切換與cpu排程
  • 并行:在同一個 時刻 ,處理多個任務,必須 多核 才能并行,真正意義的同時被執行。
  • 串行: 一組任務隻能按加入順序逐個執行,也就是後一個任務必須等待前一個任務完全執行完才能開始,最大的問題是: 如果前一個任務被暫停了即使cpu/其他資源 閑置浪費 也不能給後一個任務使用
python多線程并發_[程式設計-Python-并發程式設計]-1
python多線程并發_[程式設計-Python-并發程式設計]-1
1.3 同步? 異步? 阻塞? 非阻塞?

(參考最權威的OS教材,現代作業系統第4版)

(1) 在

程序通信

層面, 阻塞/非阻塞, 同步/異步基本是

同義詞

, 但是需要注意區分

讨論的對象

是發送方還是接收方。發送方阻塞/非阻塞(同步/異步)和接收方的阻塞/非阻塞(同步/異步) 是

互不影響

的。阻塞(同步)的含義是 主動挂起自己-

主動等待

消息到達 再做下一步操作,非阻塞(異步)的含義是 直接做下一步操作,再借助類似回調函數

被動獲得

消息;

(2) 在

IO 系統調用

層面( IO system call )層面,

非阻塞 IO

系統調用和

異步 IO

系統調用存在着一定的差别, 它們都不會阻塞程序, 但是傳回結果的方式和内容有所差别, 但是都屬于非阻塞系統調用 (non-blocing system call )。

非阻塞系統調用

(non-blocking I/O system call 與 asynchronous I/O system call) 的存在可以用來實作

線程級别的 I/O 并發

, 與通過

多程序實作的 I/O 并發

相比可以減少記憶體消耗以及程序切換的開銷。

(3) 關于

非阻塞和異步

,一個最直覺的例子: html頁面不是非要等到各種資料檔案全部加載完成才開始渲染頁面,而是一邊加載部分一邊渲染部分,即

Ajax

技術

1.4 程序? 線程? 協程?

(1) 程序: 是具有一定獨立功能的程式關于某個資料集合上的一次運作活動,是

  • 系統資源配置設定(比如記憶體) 的最小機關,
  • 每個程序都有 自己的獨立記憶體空間
  • 是以程序間 切換開銷大但是安全 (A程序崩了不影響B程序),
  • 不同程序 通過程序間通信 來通信

(2) 線程: 是程序的一個實體(一個程序内開多個線程),是

  • cpu排程 的最小機關,
  • 自己基本上不擁有系統資源,隻擁有一點在運作中必不可少的資源(如程式計數器,一組寄存器和棧), 但是它可與同屬一個程序的其他的線程 共享程序所擁有的全部資源(比如記憶體) ,
  • 是以程序切換 開銷相比程序要小,但是不安全 (因為A線程崩潰污染記憶體會影響B線程),
  • 不同線程間 主要通過共享記憶體 通信

(3) 協程: 是

使用者态的輕量級線程 (不嚴格地比喻是線上程裡面開子協程),

協程的排程完全由使用者控制。協程擁有自己的寄存器上下文和棧。協程排程切換時,将寄存器上下文和棧儲存到其他地方,在切回來的時候,恢複先前儲存的寄存器上下文和棧,

直接操作棧

基本沒有核心切換的開銷

可以不加鎖的通路全局變量

,是以

上下文的切換非常快 1.3 并發程式設計有哪些?
  • 并發主要談單核cpu ,有多程序,多線程,協程。
  • 如果是多核心cpu, 假設多核之間沒有複雜的通信,多核之間的關系就近似為 并行 ;但是如果程序(程序)數大于核心數,也需要做cpu核心的切換/排程,就出現了多程序(程序)并發
1.4 為什麼有并發(多程序/多線程/協程)?
  • 減少cpu空閑 (單線程就隻能串行地逐個執行每個程序,會存在很多的IO阻塞導緻的cpu空閑)
  • 確定任務的公平排程 (比如時間片輪轉能保證各個任務都能被公正地排程到)
  • 簡化代碼編寫,将複雜任務拆分為多個子任務并借助通信機制溝通各個子任務,使其并發執行
1.5 什麼時候 多線程 不安全?

線程安全是指在多線程環境下,程式可以始終執行正确的行為,符合預期的邏輯,這個保證在單線程環境下沒有任何問題。但是在多線程并發時,多個程序對于

共享變量的讀寫

如果不加以控制,就會出現意料之外的錯誤,這就是線程不安全的根源。

Q: 為何幾乎沒聽說過 多程序不安全呢?

TODO 1.6 如何確定 多程序/多線程安全?

多個程序對于

共享變量的讀寫

,必須要通過

互斥通路的機制

來確定線程安全。在python中,至少存在兩個機制:

  • 加鎖,也就是 對于互斥變量的讀寫的代碼段(又稱為臨界區) 程序需要 先申請鎖,再操作臨界區,最後釋放鎖,下一個線程通路臨界區
  • 消息事件通知。比如A線程通路完畢臨界區,發消息給B線程,B線程才能去通路臨界區

2. 多程序/多線程 的使用場景

2.1 通用的程式設計語言的選擇 (比如c++)
  • 任務分為 計算(cpu)密集型和IO密集型, 計算密集型任務 比如ALU裡面做的數學/邏輯運算,計數, cpu使用率非常高但幾乎不做IO,而 IO密集型任務 絕大部分時間都是在等待IO完成(剝奪cpu不影響它等待IO),比如海量圖檔讀寫/網絡端口讀寫 (最常見的比如爬蟲需要從雲端下載下傳大量的圖檔,即從遠端讀-向本地寫)
一般 web服務 主要是IO密集型,而類似 深度學習項目 則是計算密集型(GPU)
  • (1) 計算密集型,使用 多核 多線程 并行
  • (2) io密集型,使用 多核 多線程 并行加并發 (并行加并發, 并行是因為多核,并發是在單核上做多線程并發)
(1) 對于一組 cpu密集型 任務(即假設了他們幾乎無IO阻塞),總運作時間就是 每個任務的cpu執行時間的求和,沒有其他的。要降低總時間,必須給更多的cpu核心,所有任務 并行 跑,至于選擇多程序或多線程,都可以,差別是 多線程 開銷更小 多程序 更安全 一般而言 選多線程 (接下來會看到python有 特殊性

)

(2) 對于一組

io密集型 任務,每個任務的運作時間是 cpu計算時間+io時間(io時完全不需要cpu, 它就是純粹等待資料傳輸直至完成,是以 将它挂起剝奪cpu 它還是在等待 不影響

它io)。

是以一個最簡單的改進方案是,讓A任務的io時間和B任務的cpu

時間重疊 ,即A任務做io等待時讓出cpu給B任務做cpu計算 (避免cpu等待),等B任務io等待時讓出cpu給C任務做cpu計算,諸如此類。并發任務數越多,時間重疊越多, 純粹io空等-cpu空閑

的時間浪費就被減少了。

如果用是單核,選擇多程序或多線程都可以。但是多線程的線程切換開銷更小,以及多程序記憶體開銷更大,是以更考慮

多線程

如果是多核,單核上選擇多程序或多線程并發都可以,核與核之間隻要不存在複雜同步關系就能做近似并行。但是多線程的線程切換開銷更小,以及多程序記憶體開銷更大,是以更考慮

多線程

再次回顧分析 選擇多線程/多程序的核心思路:

幾個關鍵知識點

(1)減少cpu運作時間隻能是多核并行跑多個子任務

(3)cpu密集型,唯一的辦法是需要用多核并行來減小總cpu計算時間,是以多核多程序并行。

(4)io密集型,最合理的辦法是讓多任務并發,讓A任務io阻塞時間和B任務cpu計算時間重疊而不是串行先後,進而【單個任務總運作時間沒有減少,但是時間重疊,導緻多個任務總運作時間減少了,畫張overlap圖一目了然】。是以選擇單核多線程。(因為線程相比程序,記憶體占用少 ,切換開銷也小。這個時候本來就是并發的,是以gil不影響單核多線程的并發)

提高io密集型任務,理論上 多核多程序并行及并發,也是OK的,但是相比 多核多線程并行并發,記憶體開銷更大-切換排程開銷小更大,故不選

幾個點要考慮清楚: (1) 任務是 cpu密集型,io密集型;(2) 單核,多核(能并行); (3)多程序并發,多線程并發

2.2 具體到python的技術方案選擇
  • 計算密集型,選擇 多核的多程序并行 (不是并發);
  • io密集型,選擇 單核的多線程并發 (不是并行,其他語言其實應該選多核的多線程并行加并發,但python應該選單核并發)
python的多線程相比c++, 最大的不同是GIL全局鎖,這是一個最高層次全局的線程鎖。即使cpu有 多核心,每個時刻也隻有一個線程能獲得GIL,能運作 ;當發生IO時或其他定時事件到達,才放棄GIL給下一個線程使用,GIL損害的是 多核心的多線程,它還不如 單核心的多線程并發。(c++就不存在這種鎖,是以多核心能并行跑多線程,即每個核心上跑一個程序開多線程)

具體方案選擇的分析如下:

原本按照2.1中的分析,無論是 計算密集型還是io密集型任務,都應該選擇 多核的多線程并發及并行。但是由于gil影響,cpu所有核心同時隻能運作一個線程。

(1) 是以對于計算密集型,多核多線程 無法做到多核并行,是以選擇 多核多程序。(每個程序有自己的記憶體空間,互補幹擾)

(2) 對于 io 密集型任務,多核多線程的問題是,cpu0剛放出gil, 其他核也去搶但是肯定沒有cpu0成功率高,結果其他核心上的線程開始搶又沒搶到,線程狀态颠簸,性能開銷很大。是以還不如直接用 單核多線程的并發。此外其實還可以選擇 多核多程序的并發-并行,但是IO密集型本質是在等待IO而不是大量做計算,是以多核多程序 雖然花了很高記憶體及多核心的代價 但并不一定比 代價小很多的 單核多線程并發 要快。是以綜合成本效益,還是 多核多程序并發 更好

2.3 一些疑問
  • Q-1: 為啥Python要用gil全局鎖呢? 答: 控制全局共享變量的通路。記住, 鎖的發明一定為了控制 互斥變量的通路 (一般指的是 寫互斥變量, 讀一搬不互斥)
  • Q-2: 實在不知道如何分析目前任務是 計算密集型還是IO密集型,也不知道怎麼選方案,就寫兩份代碼,分别是多程序和多線程的,都跑下,做公平嚴格的控制變量的對比,哪個快用哪個

3. 協程 (TODO)

3.1 一般程式設計語言中的協程 3.2 python中協程的特殊性

4. 一點感慨

  • 在梳理這些概念過程中,發現很多很多部落格對于 準确無誤的CS基本概念 感覺是互相抄襲沒有精确求證的(其實我也差不多是這樣子。。。但是這樣不對),如果一直寫CURD code肯定沒毛病,但是一旦寫 高并發 程式估計嗝屁。關于這方面的,筆者後面還是應該重新撿起大學4大名著,所有觀點要從 經典教材(比如那邊享譽世界的現代作業系統最新版) 中去摳,而不是互相copy部落格
  • 本篇部落格肯定也有很多細節不值得推敲,忙完這個階段,要回歸 經典教材 ,重新梳理再來修正
  • 這種cs基礎,一定 先講通理論,以及普适于各種程式設計語言 的情況, 再具體到python 有啥特殊性。這才是正确的觸類旁通。

5. 參考

  • [1]. Python并發程式設計系列之常用概念剖析
  • [2]. 怎樣了解阻塞非阻塞與同步異步的差別?
  • [3]. 程序和線程、協程的差別 - RunningPower - 開發者的網上家園
  • [4]. 出于什麼樣的原因,誕生了「協程」這一概念?
  • [5]. 為什麼 Java 堅持多線程不選擇協程?
  • [6]. kelvingao:Python線程安全與臨界區
  • [7]. Python如何保證疊代list/set/dict的線程安全?
  • [8]. 宇澤:python多線程詳解
  • [9]. 為什麼多線程Python程式無法充分利用多個CPU核心帶來的優勢?