線程,有時被稱為輕量進程,是程序執行流的最小單元。一個標準的線程由線程ID,當前指令指針(PC),寄存器集合和堆棧組成。線程是進程中的一個實體,是被系統獨立調度和分派的基本單位,線程不擁有私有的系統資源,但它可與同屬一個進程的其它線程共享進程所擁有的全部資源。一個線程可以創建和撤消另一個線程,同一進程中的多個線程之間可以併發執行。
線程是程序中一個單一的順序控制流程。進程內有一個相對獨立的、可調度的執行單元,是系統獨立調度和分派CPU的基本單位指令運行時的程序的調度單位。在單個程序中同時運行多個線程完成不同的工作,稱為多線程。Python多線程用於I/O操作密集型的任務,如SocketServer網絡併發,網絡爬蟲。
現代處理器都是多核的,幾核處理器只能同時處理幾個線程,多線程執行程序看起來是同時進行,實際上是CPU在多個線程之間快速切換執行,這中間就涉及到上下問切換,所謂的上下文切換就是指一個線程Thread被分配的時間片用完了之後,線程的信息被保存起來,CPU執行另外的線程,再到CPU讀取線程Thread的信息並繼續執行Thread的過程。
線程模塊
Python的標準庫提供了兩個模塊:_thread和threading。_thread 提供了低級別的、原始的線程以及一個簡單的互斥鎖,它相比於 threading 模塊的功能還是比較有限的。Threading模塊是_thread模塊的替代,在實際的開發中,絕大多數情況下還是使用高級模塊threading,因此本書著重介紹threading高級模塊的使用。
Python創建Thread對象語法如下:
import threadingthreading.Thread(target=None, name=None, args=())
主要參數說明:
- target 是函數名字,需要調用的函數。
- name 設置線程名字。
- args 函數需要的參數,以元祖( tuple)的形式傳入
- Thread對象主要方法說明:
- run(): 用以表示線程活動的方法。
- start():啟動線程活動。
- join(): 等待至線程中止。
- isAlive(): 返回線程是否活動的。
- getName(): 返回線程名。
- setName(): 設置線程名。
Python中實現多線程有兩種方式:函數式創建線程和創建線程類。
第一種創建線程方式:
創建線程的時候,只需要傳入一個執行函數和函數的參數即可完成threading.Thread實例的創建。下面的例子使用Thread類來產生2個子線程,然後啟動2個子線程並等待其結束,
import threading import time,random,math # idx 循環次數 def printNum(idx): for num in range(idx ): #打印當前運行的線程名字 print("{0}\tnum={1}".format(threading.current_thread().getName(), num) ) delay = math.ceil(random.random() * 2) time.sleep(delay) if __name__ == '__main__': th1 = threading.Thread(target=printNum, args=(2,),name="thread1" ) th2 = threading.Thread(target=printNum, args=(3,),name="thread2" ) #啟動2個線程 th1.start() th2.start() #等待至線程中止 th1.join() th2.join() print("{0} 線程結束".format(threading.current_thread().getName()))
運行腳本得到以下結果。
thread1 num=0 thread2 num=0 thread1 num=1 thread2 num=1 thread2 num=2
MainThread 線程結束
運行腳本默認會啟動一個線程,把該線程稱為主線程,主線程有可以啟動新的線程,Python的threading模塊有個current_thread()函數,它將返回當前線程的示例。從當前線程的示例可以獲得前運行線程名字,核心代碼如下。
threading.current_thread().getName()
啟動一個線程就是把一個函數和參數傳入並創建Thread實例,然後調用start()開始執行
th1 = threading.Thread(target=printNum, args=(2,),name="thread1" ) th1.start()
從返回結果可以看出主線程示例的名字叫MainThread,子線程的名字在創建時指定,本例創建了2個子線程,名字叫thread1和thread2。如果沒有給線程起名字,Python就自動給線程命名為Thread-1,Thread-2…等等。在本例中定義了線程函數printNum(),打印idx次記錄後退出,每次打印使用time.sleep()讓程序休眠一段時間。
第二種創建線程方式:創建線程類
直接創建threading.Thread的子類來創建一個線程對象,實現多線程。通過繼承Thread類,並重寫Thread類的run()方法,在run()方法中定義具體要執行的任務。在Thread類中,提供了一個start()方法用於啟動新進程,線程啟動後會自動調用run()方法。
import threading import time,random,math class MutliThread(threading.Thread): def __init__(self, threadName,num): threading.Thread.__init__(self) self.name = threadName self.num = num def run(self): for i in range(self.num): print("{0} i={1}".format(threading.current_thread().getName(), i)) delay = math.ceil(random.random() * 2) time.sleep(delay) if __name__ == '__main__': thr1 = MutliThread("thread1",3) thr2 = MutliThread("thread2",2) # 啟動線程 thr1.start() thr2.start() # 等待至線程中止 thr1.join() thr2.join() print("{0} 線程結束".format(threading.current_thread().getName()))運行腳本得到以下結果。 thread1 i=0 thread2 i=0 thread1 i=1 thread2 i=1 thread1 i=2
MainThread 線程結束
從返回結果可以看出,通過創建Thread類來產生2個線程對象thr1和thr2,重寫Thread類的run()函數,把業務邏輯放入其中,通過調用線程對象的start()方法啟動線程。通過調用線程對象的join()函數,等待該線程完成,在繼續下面的操作。
在本例中,主線程MainThread等待子線程thread1和thread2線程運行結束後才輸出” MainThread 線程結束”。如果子線程thread1和thread2不調用join()函數,那麼主線程MainThread和2個子線程是並行執行任務的,2個子線程加上join()函數後,程序就變成順序執行了。所以子線程用到join()的時候,通常都是主線程等到其他多個子線程執行完畢後再繼續執行,其他的多個子線程並不需要互相等待。
守護線程
在線程模塊中,使用子線程對象用到join()函數,主線程需要依賴子線程執行完畢後才繼續執行代碼。如果子線程不使用join()函數,主線程和子線程是並行運行的,沒有依賴關係,主線程執行了,子線程也在執行。
在多線程開發中,如果子線程設定為了守護線程,守護線程會等待主線程運行完畢後被銷燬。一個主線程可以設置多個守護線程,守護線程運行的前提是,主線程必須存在,如果主線程不存在了,守護線程會被銷燬。
在本例中創建1個主線程3個子線程,讓主線程和子線程並行執行。內容如下。
import threading, time def run(taskName): print("任務:", taskName) time.sleep(2) print("{0} 任務執行完畢".format(taskName)) # 查看每個子線程 if __name__ == '__main__': start_time = time.time() for i in range(3): thr = threading.Thread(target=run, args=("task-{0}".format(i),)) # 把子線程設置為守護線程 thr.setDaemon(True) thr.start() # 查看主線程和當前活動的所有線程數 print("{0}線程結束,當線程數量={1}".format( threading.current_thread().getName(), threading.active_count())) print("消耗時間:", time.time() - start_time) 運行腳本得到以下結果: 任務: task-0 任務: task-1 任務: task-2 MainThread線程結束,當線程數量=4消耗時間: 0.0009751319885253906 task-2 任務執行完畢 task-0 任務執行完畢 task-1 任務執行完畢
從返回結果可以看出,當前的線程個數是4,線程個數=主線程數 + 子線程數,在本例中有1個主線程和3個子線程。主線程執行完畢後,等待子線程執行完畢,程序才會退出。
在本例的基礎上,把所有的子線程都設置為守護線程。子線程變成守護線程後,只要主線程執行完畢,程序不管子線程有沒有執行完畢,程序都會退出。使用線程對象的setDaemon(True)函數來設置守護線程。
import threading, time def run(taskName): print("任務:", taskName) time.sleep(2) print("{0} 任務執行完畢".format(taskName)) if __name__ == '__main__': start_time = time.time() for i in range(3): thr = threading.Thread(target=run, args=("task-{0}".format(i),)) # 把子線程設置為守護線程,在啟動線程前設置 thr.setDaemon(True) thr.start() # 查看主線程和當前活動的所有線程數 thrName = threading.current_thread().getName() thrCount = threading.active_count() print("{0}線程結束,當線程數量={1}".format(thrName, thrCount)) print("消耗時間:", time.time() - start_time) 運行腳本得到以下結果。 任務: task-0 任務: task-1 任務: task-2 MainThread線程結束,當線程數量=4 消耗時間: 0.0010023117065429688
從本例的返回結果可以看出,主線程執行完畢後,程序不會等待守護線程執行完畢後就退出了。設置線程對象為守護線程,一定要在線程對象調用start()函數前設置。
多線程的鎖機制
多線程編程訪問共享變量時會出現問題,但是多進程編程訪問共享變量不會出現問題。因為多進程中,同一個變量各自有一份拷貝存在於每個進程中,互不影響,而多線程中,所有變量都由所有線程共享。
多個進程之間對內存中的變量不會產生衝突,一個進程由多個線程組成,多線程對內存中的變量進行共享時會產生影響,所以就產生了死鎖問題,怎麼解決死鎖問題是本節主要介紹的內容。
1、變量的作用域
一般在函數體外定義的變量稱為全局變量,在函數內部定義的變量稱為局部變量。全局變量所有作用域都可讀,局部變量只能在本函數可讀。函數在讀取變量時,優先讀取函數本身自有的局部變量,再去讀全局變量。
內容如下。
# 全局變量 balance = 1 def change(): # 定義全局變量 global balance balance = 100 # 定義局部變量 num = 20 print("change() balance={0}".format(balance) ) if __name__ == "__main__" : change() print("修改後的 balance={0}".format(balance) ) 運行腳本得到以下結果。 change() balance=100 修改後的 balance=100
如果註釋掉change()函數里的 global
v1,那麼得到的返回值是。change() balance=100修改後的 balance=1
在本例中在change()函數外定義的變量balance是全局變量,在change()函數內定義的變量num是局部變量,全局變量默認是可讀的,可以在任何函數中使用,如果需要改變全局變量的值,需要在函數內部使用global定義全局變量,本例中在change()函數內部使用global定義全局變量balance,在函數里就可以改變全局變量了。
在函數里可以使用全局變量,但是在函數里不能改變全局變量。想實現多個線程共享變量,需要使用全局變量。在方法里加上全局關鍵字 global定義全局變量,多線程才可以修改全局變量來共享變量。
2、多線程中的鎖
多線程同時修改全局變量時會出現數據安全問題,線程不安全就是不提供數據訪問保護,有可能出現多個線程先後更改數據造成所得到的數據是髒數據。在本例中我們生成2個線程同時修改change()函數里的全局變量balance時,會出現數據不一致問題。
本案例文件名為PythonFullStack\Chapter03\threadDemo03.py,內容如下。
import threading balance = 100 def change(num, counter): global balance for i in range(counter): balance += num balance -= num if balance != 100: # 如果輸出這句話,說明線程不安全 print("balance=%d" % balance) break if __name__ == "__main__": thr1 = threading.Thread(target=change,args=(100,500000),name='t1') thr2 = threading.Thread(target=change,args=(100,500000),name='t2') thr1.start() thr2.start() thr1.join() thr2.join() print("{0} 線程結束".format(threading.current_thread().getName()))運行以上腳本,當2個線程運行次數達到500000次時,會出現以下結果。 balance=200 MainThread 線程結束
在本例中定義了一個全局變量balance,初始值為100,當啟動2個線程後,先加後減,理論上balance應該為100。線程的調度是由操作系統決定的,當線程t1和t2交替執行時,只要循環次數足夠多,balance結果就不一定是100了。從結果可以看出,在本例中線程t1和t2同時修改全局變量balance時,會出現數據不一致問題。
注意
在多線程情況下,所有的全局變量有所有線程共享。所以,任何一個變量都可以被任何一個線程修改,因此,線程之間共享數據最大的危險在於多個線程同時改一個變量,把內容給改亂了。
在多線程情況下,使用全局變量並不會共享數據,會出現線程安全問題。線程安全就是多線程訪問時,採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其他線程不能進行訪問直到該線程讀取完,其他線程才可使用。不會出現數據不一致
在單線程運行時沒有代碼安全問題。寫多線程程序時,生成一個線程並不代表多線程。在多線程情況下,才會出現安全問題。
針對線程安全問題,需要使用”互斥鎖”,就像數據庫裡操縱數據一樣,也需要使用鎖機制。某個線程要更改共享數據時,先將其鎖定,此時資源的狀態為“鎖定”,其他線程不能更改;直到該線程釋放資源,將資源的狀態變成“非鎖定”,其他的線程才能再次鎖定該資源。互斥鎖保證了每次只有一個線程進行寫入操作,從而保證了多線程情況下數據的正確性。
互斥鎖的核心代碼如下:
# 創建鎖 mutex = threading.Lock() # 鎖定 mutex.acquire() # 釋放 mutex.release()
如果要確保balance計算正確,使用threading.Lock()來創建鎖對象lock,把 lock.acquire()和lock.release()加在同步代碼塊裡,本例的同步代碼塊就是對全局變量balance進行先加後減操作。
當某個線程執行change()函數時,通過lock.acquire()獲取鎖,那麼其他線程就不能執行同步代碼塊了,只能等待知道鎖被釋放了,獲得鎖才能執行同步代碼塊。由於鎖只有一個,無論多少線程,同一個時刻最多隻有一個線程持有該鎖,所以修改全局變量balance不會產生衝突。改良後的代碼內容如下。
import threading balance = 100 lock = threading.Lock() def change(num, counter): global balance for i in range(counter): # 先要獲取鎖 lock.acquire() balance += num balance -= num # 釋放鎖 lock.release() if balance != 100: # 如果輸出這句話,說明線程不安全 print("balance=%d" % balance) break if __name__ == "__main__": thr1 = threading.Thread(target=change,args=(100,500000),name='t1') thr2 = threading.Thread(target=change,args=(100,500000),name='t2') thr1.start() thr2.start() thr1.join() thr2.join() print("{0} 線程結束".format(threading.current_thread().getName()))
在本例中2個線程同時運行lock.acquire()時,只有一個線程能成功的獲取鎖,然後執行代碼,其他線程就繼續等待直到獲得鎖位置。獲得鎖的線程用完後一定要釋放鎖,否則其他線程就會一直等待下去,成為死線程。
在運行上面腳本就不會產生輸出信息,證明代碼是安全的。把 lock.acquire()和lock.release()加在同步代碼塊裡,還要注意鎖的力度不要加的太大了。第一個線程只有運行完了,第二個線程才能運行,所以鎖要在需要同步代碼里加上。
歡迎大家關注我的頭條號,私信“python",學習資料包免費分享給需要的朋友,另有python學習交流群,可以交流學習拓展人脈。