程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式師世界 >> 編程語言 >  >> 更多編程語言 >> Python

python 線程初窺

編輯:Python

1. 引言

上一篇文章中,我們詳細介紹了 python 中的協程。 一文講透 python 協程

python 通過 yeild 關鍵字讓出 CPU 的執行,實現類似於系統中斷的並發工作,這就是被稱為“微線程”的 python 協程調度機制。 但是,這並不是真正意義上的並發,幾乎在所有編程語言中,都提供了多線程並發的機制,python 也同樣提供了多線程並發機制,本文我們就來詳細介紹 python 中的線程機制。

2. python 中的線程

2.1. 操作系統中的線程

此前,我們曾經介紹過 linux 環境中的線程和相關的 api。

一個程序的一次執行就是一個進程,而進程中可能包含多個線程,線程是 CPU 調度的最小單位,同一個進程中的多個線程共享了進程中的程序文本、全局內存和堆內存、棧以及文件描述符等資源,而同一個計算機上的多個進程則共享文件系統、磁盤、打印機等硬件資源。 線程的切換相對於進程切換耗費的資源更少,因此效率更高,尤其是在同一個進程中的若干個線程之間切換,這是因為進程的切換需要執行系統陷阱、上下文切換和內存與高速緩存的刷新,而由於同一進程中的多個線程共享了這些資源,在線程切換過程中,系統無需對這些資源進行任何操作,因此可以獲得更高的效率。 可見,線程調度是程序設計中一個非常重要且實用的技術。

2.2. thread 與 threading

python 標准庫中維護線程的模塊有兩個 — thread 和 threading。 由於 thread。 模塊在很多方面存在不盡如人意的問題,例如在多線程並發環境中,當主線程退出時,所有子線程會隨之立即退出,甚至不會進行任何清理工作,這通常是無法接受的,所以一般並不建議使用。 在 python3 中 thread 模塊已經被更名為 _thread 模塊,以便從名字上說明其不被推薦使用。 如果你熟悉 java 的線程模型,你會發現 python 的線程模型與 java 的非常類似,沒錯,python 的線程模型就是參照 java 線程模型設計的,但 python 的線程目前還沒有優先級,沒有線程組,線程還不能被銷毀、停止、暫停、恢復或中斷。

2.2.1. threading 模塊中的類

threading 模塊包含下列對象:

threading 模塊中的對象

對象

描述

Thread

執行線程對象

Timer

運行前等待一定時間的執行線程對象

Lock

鎖對象

Condition

條件變量對象,用於描述線程同步中的條件變量

Event

事件對象,用於描述線程同步中的事件

Semaphore

信號量對象,用於描述線程同步中的計數器

BoundedSemaphore

存在阈值信號量對象

Barrier

柵欄對象,線程同步中讓多個線程執行到指定位置

3. Thread 類

threading 模塊中最重要的類就是 Thread 類。 每個 Thread 對象就是一個線程。 下面是 Thread 類中包含的屬性和方法。

Thread 類屬性及成員方法

屬性

備注

name

線程名稱

ident

線程標識符

deamon

bool 類型,表示該線程是否為守護線程

start()

開始執行線程

run()

用於定義線程功能,通常在子類中由開發者復寫

join(timeout=None)

直到啟動的線程終止或到超時時間前一直掛起

is_alive()

返回 bool 類型,表示該線程是否存活

4. python 線程的創建與終止

4.1. 創建線程

有兩種方法可以創建線程,但更推薦第二種:

4.1.1. 以一個函數或一個可調用類實例為參數創建 Thread 對象

from threading import Thread
from time import sleep, ctime
def sleep_func(i):
print('start_sleep[%s]' % i)
sleep(i+1)
print('end_sleep[%s]' % i)
if __name__ == '__main__':
print('start at %s'% ctime())
threads = list()
for i in range(3):
t = Thread(target=sleep_func, args=[i])
threads.append(t)
for i in range(3):
threads[i].start()
for i in range(3):
threads[i].join()
print('end at %s' % ctime())

打印出了:

start at Mon May 6 12:25:18 2019 start_sleep[0] start_sleep[1] start_sleep[2] end_sleep[0] end_sleep[1] end_sleep[2] end at Mon May 6 12:25:21 2019 並且我們觀察到每過 1 秒便打印出一個 end_sleep,這說明他們確實是並行執行的。 本應共執行 1+2+3 = 6 秒的,由於線程的並發執行,實際只用了 3 秒。

4.1.2. 派生 Thread 並創建子類實例

from threading import Thread
from time import sleep, ctime
class myThread(Thread):
def __init__(self, nsec):
super().__init__()
self.nsec = nsec
def run(self):
print('start_sleep[%s]' % self.nsec)
sleep(self.nsec + 1)
print('end_sleep[%s]' % self.nsec)
if __name__ == '__main__':
print('start at %s'% ctime())
threads = list()
for i in range(5):
t = myThread(i)
threads.append(t)
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print('end at %s' % ctime())

運行與上面通過函數實現的例子是完全一致的。 由於類中可以添加私有成員來保存成員方法運行結果或其他數據,這個方法顯得更為靈活。

4.1.3. start 方法

只有在 Thread 對象的 start 方法被調用後才會開始線程活動。 start 方法在一個線程裡最多只能被調用一次,否則會拋出 RuntimeError。 start 最終執行的邏輯代碼就是 Thread 類的 run 方法。

4.1.4. join 方法

join 方法有一個可選的 timeout 參數,這個方法會阻塞調用這個方法的線程,直到被調用 join() 的線程終結或達到 timeout 秒數。 當然,一個線程可以被 join 很多次,但 join 當前線程會導致死鎖。 如果被 join 的線程不處於 alive 狀態,則會引起 RuntimeError 異常。

4.2. 線程的終止

在線程中,可以通過 sys.exit() 方法或拋出 SystemExit 異常來使線程退出。 但是,在線程外,你不能直接終止一個線程。

5. threading 模塊提供的函數

除了最重要的 Thread 類,threading 模塊中還提供了下面的幾個有用的函數。

threading 模塊提供的函數

函數

說明

active_count()

返回當前活動的 Thread 對象個數

current_thread()

返回當前線程的 Thread 對象

enumerate()

返回當前活動的 Thread 對象列表

settrace(func)

為所有線程設置一個 trace 函數

setprofile(func)

為所有線程設置一個 profile 函數

local()

創建或獲取線程本地數據對象

stack_size(size=0)

返回新創建線程的棧大小或為後續創建的線程設定棧的大小 為 size

get_ident()

返回當前線程的 “線程標識符”,它是一個非零的整數

main_thread()

返回主 Thread 對象。一般情況下,主線程是Python解釋器開始時創建的線程

6. python 線程與 GIL

上面我們通過一個實際的例子已經看到,三個線程分別 sleep 1、2、3 秒,執行結果卻只話費了 3 秒,足以見得並發環境下的性能優勢。 然而,眾所周知,python 解釋器有多個版本的實現,其中 CPython 以其優秀的執行效率而被廣泛使用,也成為了 python 的默認解釋器,另一個被廣泛使用的是 PyPy 解釋器,這兩個解釋器都有一個先天缺陷,那就是並非線程安全,這是出於在高性能 和復雜度之間做出的讓步。 由於 python 解釋器 CPython、PyPy 的實現限制,導致實際執行中會設置全局解釋安全鎖(GIL),一次只允許使用一個線程執行 Python 字節碼,因此,一個 Python 進程通常不能同時使用多個 CPU 核心,多線程的程序也並不總是真的在並發執行的,但這並不是 python 語言本身的限制,Jython 與 IronPython 並沒有這樣的限制。 即便如此,所有標准庫中的阻塞式 IO 操作,在等待操作系統返回結果時都會釋放 GIL,因此對於 IO 密集型程序,使用多線程並發是可以有效提升性能的,例如我們可以讓多個線程可以同時等待或接收 IO操作的返回數據或者在一個線程執行下載任務的同時,另一個線程負責顯示下載進度。 time.sleep 操作也是一樣,time.sleep 操作會立即釋放 GIL 鎖,並讓線程阻塞等待參數傳入的秒數,直到此後才再次請求獲取 GIL 鎖,這就是上文例子中多線程並發縮短了執行時間的原因。 但對於 CPU 密集型程序,python 線程則顯得有些無力,不過這並不是沒有辦法去優化,我們後文會詳細介紹。

6.1. 並發計算斐波那契數列

斐波那契數列的計算是一個典型的 CPU 密集型操作,下面的例子展示了分別在串行環境與並發環境下計算10次斐波那契數列第 100000 個元素的耗時:

import time
from threading import Thread
class fibThread(Thread):
def __init__(self, stop):
super().__init__()
self.stop = stop
self.result = None
def run(self):
self.result = fib(self.stop)
def fib(stop):
a = 0
b = 1
for _ in range(stop):
a, b = a + b, a
return a
if __name__ == '__main__':
stime = time.time()
for _ in range(10):
fib(100000)
print('serial time %ss' % (time.time() - stime))
stime = time.time()
threads = list()
for _ in range(10):
t = fibThread(100000)
threads.append(t)
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print('concurrent time %ss' % (time.time() - stime))

打印出了:

serial time 0.9864494800567627s concurrent time 0.9564151763916016s

雖然從結果上,多線程並發確實有著略微的性能提升,但遠遠沒有達到我們預期的優化 90% 這個問題如何進一步優化,敬請關注接下來的文章。

7. 參考資料

《python 核心編程》。 https://docs.python.org/zh-cn/3.6/library/threading.html。


  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式師世界 All Rights Reserved