溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Python進程和線程知識點舉例分析

發(fā)布時間:2021-11-20 14:18:44 來源:億速云 閱讀:129 作者:iii 欄目:編程語言

本篇內容主要講解“Python進程和線程知識點舉例分析”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Python進程和線程知識點舉例分析”吧!

多線程

一個進程至少包含一個線程,其實進程就是由若干個線程組成的。線程是操作系統(tǒng)直接支持的執(zhí)行單元,因此高級語言通常都內置多線程的支持,Python 也不例外,而且Python 的線程是真正的 Posix Thread ,而不是模擬出來的線程。

多線程的運行有如下優(yōu)點:

  • 使用線程可以把占據長時間的程序中的任務放到后臺去處理。

  • 用戶界面可以更加吸引人,比如用戶點擊了一個按鈕去觸發(fā)某些事件的處理,可以彈出一個進度條來顯示處理的進度。

  • 程序的運行速度可能加快。

  • 在一些等待的任務實現上如用戶輸入、文件讀寫和網絡收發(fā)數據等,線程就比較有用了。在這種情況下我們可以釋放一些珍貴的資源如內存占用等等。

線程可以分為:

  • 內核線程:由操作系統(tǒng)內核創(chuàng)建和撤銷。

  • 用戶線程:不需要內核支持而在用戶程序中實現的線程。

Python 的標準庫提供了兩個模塊:_thread 和 threading,前者是低級模塊,后者是高級模塊,對 _thread 進行了封裝。大多數情況只需要采用 threading模塊即可,并且也推薦采用這個模塊。

這里再次以下載文件作為例子,用多線程的方式來實現一遍:

from random import randint
from threading import Thread, current_thread
from time import time, sleep
def download(filename):
 print('thread %s is running...' % current_thread().name)
 print('開始下載%s...' % filename)
 time_to_download = randint(5, 10)
 sleep(time_to_download)
 print('%s下載完成! 耗費了%d秒' % (filename, time_to_download))
def download_multi_threading():
 print('thread %s is running...' % current_thread().name)
 start = time()
 t1 = Thread(target=download, args=('Python.pdf',), name='subthread-1')
 t1.start()
 t2 = Thread(target=download, args=('nazha.mkv',), name='subthread-2')
 t2.start()
 t1.join()
 t2.join()
 end = time()
 print('總共耗費了%.3f秒' % (end - start))
 print('thread %s is running...' % current_thread().name)
if __name__ == '__main__':
 download_multi_threading()

實現多線程的方式和多進程類似,也是通過 Thread 類創(chuàng)建線程對象,target 參數表示傳入需要執(zhí)行的函數,args 參數是表示傳給函數的參數,然后 name 是給當前線程進行命名,默認命名是如 Thread- 1、Thread-2 等等。

此外,任何進程默認會啟動一個線程,我們將它稱為主線程,主線程又可以啟動新的線程,在 threading 模塊中有一個函數 current_thread() ,可以返回當前線程的實例。主線程實例的名字叫 MainThread,子線程的名字是在創(chuàng)建的時候指定,也就是 name 參數。

運行結果:

thread MainThread is running...
thread subthread-1 is running...
開始下載Python.pdf...
thread subthread-2 is running...
開始下載nazha.mkv...
nazha.mkv下載完成! 耗費了5秒
Python.pdf下載完成! 耗費了7秒
總共耗費了7.001秒
thread MainThread is running...

Lock

多線程和多進程最大的不同在于,多進程中,同一個變量,各自有一份拷貝存在于每個進程中,互不影響,而多線程中,所有變量都由所有線程共享,所以,任何一個變量都可以被任何一個線程修改,因此,線程之間共享數據最大的危險在于多個線程同時改一個變量,把內容給改亂了。

下面是一個例子,演示了多線程同時操作一個變量,如何把內存給改亂了:

from threading import Thread
from time import time, sleep
# 假定這是你的銀行存款:
balance = 0
def change_it(n):
 # 先存后取,結果應該為0:
 global balance
 balance = balance + n
 balance = balance - n
def run_thread(n):
 for i in range(100000):
 change_it(n)
def nolock_multi_thread():
 t1 = Thread(target=run_thread, args=(5,))
 t2 = Thread(target=run_thread, args=(8,))
 t1.start()
 t2.start()
 t1.join()
 t2.join()
 print(balance)
if __name__ == '__main__':
 nolock_multi_thread()

運行結果:

-8

代碼中定義了一個共享變量 balance,然后啟動兩個線程,先存后取,理論上結果應該是 0 。但是,由于線程的調度是由操作系統(tǒng)決定的,當 t1、t2 交替執(zhí)行時,只要循環(huán)次數足夠多,balance 的結果就不一定是0了。

原因就是下面這條語句:

balance = balance + n

這條語句的執(zhí)行分為兩步的:

  • 先計算 balance + n,保存結果到一個臨時變量

  • 將臨時變量的值賦給 balance

也就是可以看成:

x = balance+n
balance=x

正常運行如下所示:

初始值 balance = 0
t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t1: balance = x1 # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1 # balance = 0
t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2 # balance = 8
t2: x2 = balance - 8 # x2 = 8 - 8 = 0
t2: balance = x2 # balance = 0
結果 balance = 0

但實際上兩個線程是交替運行的,也就是:

初始值 balance = 0
t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2 # balance = 8
t1: balance = x1 # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1 # balance = 0
t2: x2 = balance - 8 # x2 = 0 - 8 = -8
t2: balance = x2 # balance = -8
結果 balance = -8

簡單說,就是因為對 balance 的修改需要多條語句,而執(zhí)行這幾條語句的時候,線程可能中斷,導致多個線程把同個對象的內容該亂了。

要保證計算正確,需要給 change_it() 添加一個鎖,添加鎖后,其他線程就必須等待當前線程執(zhí)行完并釋放鎖,才可以執(zhí)行該函數。并且鎖是只有一個,無論多少線程,同一時刻最多只有一個線程持有該鎖。通過 threading 模塊的 Lock 實現。

因此代碼修改為:

from threading import Thread, Lock
from time import time, sleep
# 假定這是你的銀行存款:
balance = 0
lock = Lock()
def change_it(n):
 # 先存后取,結果應該為0:
 global balance
 balance = balance + n
 balance = balance - n
def run_thread_lock(n):
 for i in range(100000):
 # 先要獲取鎖:
 lock.acquire()
 try:
 # 放心地改吧:
 change_it(n)
 finally:
 # 改完了一定要釋放鎖:
 lock.release()
def nolock_multi_thread():
 t1 = Thread(target=run_thread_lock, args=(5,))
 t2 = Thread(target=run_thread_lock, args=(8,))
 t1.start()
 t2.start()
 t1.join()
 t2.join()
 print(balance)
if __name__ == '__main__':
 nolock_multi_thread()

但遺憾的是 Python 并不能完全發(fā)揮多線程的作用,這里可以通過寫一個死循環(huán),然后通過任務管理器查看進程的 CPU 使用率。

正常來說,如果有兩個死循環(huán)線程,在多核CPU中,可以監(jiān)控到會占用200%的CPU,也就是占用兩個CPU核心。

要想把 N 核CPU的核心全部跑滿,就必須啟動 N 個死循環(huán)線程。

死循環(huán)代碼如下所示:

import threading, multiprocessing
def loop():
 x = 0
 while True:
 x = x ^ 1
for i in range(multiprocessing.cpu_count()):
 t = threading.Thread(target=loop)
 t.start()

在 4 核CPU上可以監(jiān)控到 CPU 占用率僅有102%,也就是僅使用了一核。

但是用其他編程語言,比如C、C++或 Java來改寫相同的死循環(huán),直接可以把全部核心跑滿,4核就跑到400%,8核就跑到800%,為什么Python不行呢?

因為 Python 的線程雖然是真正的線程,但解釋器執(zhí)行代碼時,有一個 GIL鎖:Global Interpreter Lock,任何Python線程執(zhí)行前,必須先獲得GIL鎖,然后,每執(zhí)行100條字節(jié)碼,解釋器就自動釋放GIL鎖,讓別的線程有機會執(zhí)行。這個 GIL 全局鎖實際上把所有線程的執(zhí)行代碼都給上了鎖,所以,多線程在Python中只能交替執(zhí)行,即使100個線程跑在100核CPU上,也只能用到1個核。

GIL是 Python 解釋器設計的歷史遺留問題,通常我們用的解釋器是官方實現的 CPython,要真正利用多核,除非重寫一個不帶GIL的解釋器。

盡管多線程不能完全利用多核,但對于程序的運行效率提升還是很大的,如果想實現多核任務,可以通過多進程實現多核任務。多個Python進程有各自獨立的GIL鎖,互不影響。

ThreadLocal

采用多線程的時候,一個線程采用自己的局部變量會比全局變量更好,原因前面也介紹了,如果不加鎖,多個線程可能會亂改某個全局變量的數值,而局部變量是只有每個線程自己可見,不會影響其他線程。

不過,局部變量的使用也有問題,就是函數調用時候,傳遞起來會比較麻煩,即如下所示:

def process_student(name):
 std = Student(name)
 # std是局部變量,但是每個函數都要用它,因此必須傳進去:
 do_task_1(std)
 do_task_2(std)
def do_task_1(std):
 do_subtask_1(std)
 do_subtask_2(std)
def do_task_2(std):
 do_subtask_2(std)
 do_subtask_2(std)

局部變量需要一層層傳遞給每個函數,比較麻煩,有沒有更好的辦法呢?

一個思路是用一個全局的 dict ,然后用每個線程作為 key ,代碼例子如下所示:

global_dict = {}
def std_thread(name):
 std = Student(name)
 # 把std放到全局變量global_dict中:
 global_dict[threading.current_thread()] = std
 do_task_1()
 do_task_2()
def do_task_1():
 # 不傳入std,而是根據當前線程查找:
 std = global_dict[threading.current_thread()]
 ...
def do_task_2():
 # 任何函數都可以查找出當前線程的std變量:
 std = global_dict[threading.current_thread()]

這種方式理論上是可行的,它可以避免局部變量在每層函數中傳遞,只是獲取局部變量的代碼不夠優(yōu)雅,在 threading 模塊中提供了 local 函數,可以自動完成這件事情,代碼如下所示:

import threading
# 創(chuàng)建全局ThreadLocal對象:
local_school = threading.local()
def process_student():
 # 獲取當前線程關聯的student:
 std = local_school.student
 print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
 # 綁定ThreadLocal的student:
 local_school.student = name
 process_student()
t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()

運行結果:

Hello, Alice (in Thread-A)
Hello, Bob (in Thread-B)

在代碼中定義了一個全局變量 local_school ,它是一個 ThreadLocal 對象,每個線程都可以對它讀寫 student 屬性,但又不會互相影響,也不需要管理鎖的問題,這是 ThreadLocal 內部會處理。

ThreadLocal 最常用的是為每個線程綁定一個數據庫連接,HTTP 請求,用戶身份信息等,這樣一個線程的所有調用到的處理函數都可以非常方便地訪問這些資源。

進程 vs 線程

我們已經分別介紹了多進程和多線程的實現方式,那么究竟應該選擇哪種方法來實現并發(fā)編程呢,這兩者有什么優(yōu)缺點呢?

通常多任務的實現,我們都是設計 Master-Worker,Master 負責分配任務,Worker 負責執(zhí)行任務,因此多任務環(huán)境下,通常是一個 Master 和多個 Worker。

如果用多進程實現 Master-Worker,主進程就是 Master,其他進程就是 Worker。

如果用多線程實現 Master-Worker,主線程就是 Master,其他線程就是 Worker。

對于多進程,最大的優(yōu)點就是穩(wěn)定性高,因為一個子進程掛了,不會影響主進程和其他子進程。當然主進程掛了,所有進程自然也就掛,但主進程只是負責分配任務,掛掉概率非常低。著名的 Apache 最早就是采用多進程模式。

缺點有:

  • 創(chuàng)建進程代價大,特別是在 windows 系統(tǒng),開銷巨大,而 Unix/ Linux 系統(tǒng)因為可以調用 fork() ,所以開銷還行;

  • 操作系統(tǒng)可以同時運行的進程數量有限,會受到內存和 CPU 的限制。

對于多線程,通常會快過多進程,但也不會快太多;缺點就是穩(wěn)定性不好,因為所有線程共享進程的內存,一個線程掛斷都可能直接造成整個進程崩潰。比如在Windows上,如果一個線程執(zhí)行的代碼出了問題,你經??梢钥吹竭@樣的提示:“該程序執(zhí)行了非法操作,即將關閉”,其實往往是某個線程出了問題,但是操作系統(tǒng)會強制結束整個進程。

進程/線程切換

是否采用多任務模式,第一點需要注意的就是,一旦任務數量過多,效率肯定上不去,這主要是切換進程或者線程是有代價的。

操作系統(tǒng)在切換進程或者線程時的流程是這樣的:

  • 先保存當前執(zhí)行的現場環(huán)境(CPU寄存器狀態(tài)、內存頁等)

  • 然后把新任務的執(zhí)行環(huán)境準備好(恢復上次的寄存器狀態(tài),切換內存頁等)

  • 開始執(zhí)行任務

這個切換過程雖然很快,但是也需要耗費時間,如果任務數量有上千個,操作系統(tǒng)可能就忙著切換任務,而沒有時間執(zhí)行任務,這種情況最常見的就是硬盤狂響,點窗口無反應,系統(tǒng)處于假死狀態(tài)。

計算密集型vsI/O密集型

采用多任務的第二個考慮就是任務的類型,可以將任務分為計算密集型和 I/O 密集型。

計算密集型任務的特點是要進行大量的計算,消耗CPU資源,比如對視頻進行編碼解碼或者格式轉換等等,這種任務全靠 CPU 的運算能力,雖然也可以用多任務完成,但是任務越多,花在任務切換的時間就越多,CPU 執(zhí)行任務的效率就越低。計算密集型任務由于主要消耗CPU資源,這類任務用 Python這樣的腳本語言去執(zhí)行效率通常很低,最能勝任這類任務的是C語言,我們之前提到了 Python 中有嵌入 C/C++ 代碼的機制。不過,如果必須用 Python 來處理,那最佳的就是采用多進程,而且任務數量最好是等同于 CPU 的核心數。

除了計算密集型任務,其他的涉及到網絡、存儲介質 I/O 的任務都可以視為 I/O 密集型任務,這類任務的特點是 CPU 消耗很少,任務的大部分時間都在等待 I/O 操作完成(因為 I/O 的速度遠遠低于 CPU 和內存的速度)。對于 I/O 密集型任務,如果啟動多任務,就可以減少 I/O 等待時間從而讓 CPU 高效率的運轉。一般會采用多線程來處理 I/O 密集型任務。

異步 I/O

現代操作系統(tǒng)對 I/O 操作的改進中最為重要的就是支持異步 I/O。如果充分利用操作系統(tǒng)提供的異步 I/O 支持,就可以用單進程單線程模型來執(zhí)行多任務,這種全新的模型稱為事件驅動模型。Nginx 就是支持異步 I/O的 Web 服務器,它在單核 CPU 上采用單進程模型就可以高效地支持多任務。在多核 CPU 上,可以運行多個進程(數量與CPU核心數相同),充分利用多核 CPU。用 Node.js 開發(fā)的服務器端程序也使用了這種工作模式,這也是當下實現多任務編程的一種趨勢。

在 Python 中,單線程+異步 I/O 的編程模型稱為協(xié)程,有了協(xié)程的支持,就可以基于事件驅動編寫高效的多任務程序。協(xié)程最大的優(yōu)勢就是極高的執(zhí)行效率,因為子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷。協(xié)程的第二個優(yōu)勢就是不需要多線程的鎖機制,因為只有一個線程,也不存在同時寫變量沖突,在協(xié)程中控制共享資源不用加鎖,只需要判斷狀態(tài)就好了,所以執(zhí)行效率比多線程高很多。如果想要充分利用CPU的多核特性,最簡單的方法是多進程+協(xié)程,既充分利用多核,又充分發(fā)揮協(xié)程的高效率,可獲得極高的性能。

到此,相信大家對“Python進程和線程知識點舉例分析”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續(xù)學習!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI