python3 多線程篇1

1 前言

雖然說Python的運行效率比不過像C++/Java這樣的大哥,但是其代碼簡練、相關數據處理工具包多、開發快等特點真的很誘人。之前一直用Python寫單進程程序,但最近遇到的數據大小各個上G,如果使用for循環一個一個執行,那真得等到猴年馬月了。遂本人學習了一下Python中的多線程/多進程知識,經過一番學習找到了Python中編寫並行程序的最好模式,記錄如下:

Python3 通過兩個標準庫 _thread 和 threading 提供對線程的支持,不過threading基本可以完虐_thread,_thread 提供了低級別的、原始的線程以及一個簡單的鎖,它相比於 threading 模塊的功能還是比較有限的。因此重點看一下threading的使用方法就行了。

相比於_thread,threading中多瞭如下函數:

  • threading.currentThread(): 返回當前的線程變量。
  • threading.enumerate(): 返回一個包含正在運行的線程的list。
  • threading.activeCount(): 返回正在運行的線程數量。

例如,我在Jupyter 中運行如下程序可能會得到如下結果:

import threading

print(threading.enumerate()

"""
[<Thread(Thread-4, started daemon 22692)>,
 <Heartbeat(Thread-5, started daemon 11832)>,
 <_MainThread(MainThread, started 16308)>,
 <HistorySavingThread(IPythonHistorySavingThread, started 1100)>,
 <ParentPollerWindows(Thread-3, started daemon 21508)>]
 """

2 我們開始吧

Python中使用線程有兩種方式:函數或者用類來包裝線程對象,在本小節先介紹函數式用法,下一小節介紹類的用法。廢話不多少,直接通過下面的示例代碼介紹如何使用threading 編寫多線程程序,先看代碼:

import threading
import time

def thread_func(param1, param2):
	for i in range(5):
		print("thread_func called! i = %d" % i)
		time.sleep(1)
	print("thread_func finished! param1 = %s, param2 = %s" % (param1, param2))

def main():
	print("main func start!")
	new_thread = threading.Thread(target=thread_func, args = (1, 2))
	new_thread.start()
	print("main func finish!")

if __name__ == "__main__":
	main()

接下來簡單的介紹一下上面的代碼,在main函數中創建一個線程new_thread,通過觀察法能夠總結出線程的創建方式爲:

threading.Thread(target=EXECUTE_FUNC, args = PARAMETERS)

其中target給定這個線程執行的函數名稱,PARAMETERS是傳遞給線程函數的參數,是個tuple類型。

創建線程之後需要調用 start() 方法啓動線程,上面的代碼執行結果如下:

main func start!
thread_func called! i = 0
main func finish!
thread_func called! i = 1
thread_func called! i = 2
thread_func called! i = 3
thread_func called! i = 4
thread_func finished! param1 = 1, param2 = 2

可以看到,在啓動線程之後main函數和thread_func便"並行"執行了,而且main函數先於thread_func結束。其實有時候需要讓main函數阻塞,等待new_thread這個線程執行完成之後再繼續執行後面的代碼,這時候就需要join()函數閃亮登場啦!執行join方法會阻塞調用線程,直到調用join方法的線程結束。

使用起來很簡單,只需要在前面的代碼基礎上添加一句即可:

import time

def thread_func(param1, param2):
    for i in range(5):
        print("thread_func called! i = %d" % i)
        time.sleep(1)
    print("thread_func finished! param1 = %s, param2 = %s" % (param1, param2))

def main():
    print("main func start!")
    new_thread = threading.Thread(target=thread_func, args = (1, 2))
    new_thread.start()
    new_thread.join()
    print("main func finish!")

if __name__ == "__main__":
    main()

上面的代碼執行結果爲:

main func start!
thread_func called! i = 0
thread_func called! i = 1
thread_func called! i = 2
thread_func called! i = 3
thread_func called! i = 4
thread_func finished! param1 = 1, param2 = 2
main func finish!

嗯,一切都符合預期~

3 使用類繼承創建線程類

可以通過直接從 threading.Thread 繼承創建一個新的子類,在run函數中編寫代碼確定線程的執行邏輯。在實例化後調用 start() 方法啓動新線程,它會調用 run() 方法,直接看個例子就懂了:

import threading
import time

class myThread (threading.Thread):
    def __init__(self, threadID, name, counter):
        # 重寫threading.Thread的__init__方法時,確保在所有操作之前先調用threading.Thread.__init__方法
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.counter = counter
        
    def run(self):
        print ("開始線程:" + self.name)
        for i in range(5):
            print("thread-%s called!" % self.name)
            time.sleep(1)
        print ("退出線程:" + self.name)

def main():
    # 創建新線程
    thread1 = myThread(1, "Thread-1", 1)
    thread2 = myThread(2, "Thread-2", 2)

    # 開啓新線程
    thread1.start()
    thread2.start()
    thread1.join()
    thread2.join()
    print ("退出主線程")
    
if __name__ == "__main__":
    main()

4 線程同步

既然有多線程,肯定就會存在互斥、同步的問題,可以使用 Thread 對象的 Lock 和 Rlock 可以實現簡單的線程同步,這兩個對象都有 acquire 方法和 release 方法,對於那些需要每次只允許一個線程操作的數據,可以將其操作放到 acquire 和 release 方法之間。

4.1 threading.Lock()

threading.Lock是直接通過_thread模塊擴展實現的。當鎖在被鎖定時,它並不屬於某一個特定的線程。也就是說鎖只有“鎖定”和“非鎖定”兩種狀態,當鎖被創建時,是處於“非鎖定”狀態的。當鎖已經被鎖定時,其他線程再次調用acquire()方法會被阻塞執行,直到鎖被獲得鎖的線程調用release()方法釋放掉鎖並將其狀態改爲“非鎖定”。

使用示例:

threadLock = threading.Lock()

def thread_func():
	threadLock.acquire()
	do_something()
	threadLock.release()

4.2 threading.RLock()

遞歸鎖和普通鎖的差別在於加入了“所屬線程”和“遞歸等級”的概念,釋放鎖必須有獲取鎖的線程來進行釋放,同時,同一個線程在釋放鎖之前再次獲取鎖將不會阻塞當前線程,只是在鎖的遞歸等級上加了1(獲得鎖時的初始遞歸等級爲1)。

使用普通鎖時,對於一些可能造成死鎖的情況,可以考慮使用遞歸鎖來解決。

5 python中的GIL

由於python是解釋型語言,它在運行的時候需要解釋器,簡單描述下GIL,即global interpreter lock,全局解釋器鎖,就是python在運行的時候會鎖定解釋器,就是說在運行的時候只能是一個線程,鎖死了,切換不了。因此每個線程在運行之前都要申請GIL,那麼就必須要等上一個線程釋放這把鎖你纔可以申請到,然後執行代碼。由於GIL的存在,在計算密集型應用場景可能使用python多線程的執行效率還不如單線程,在處理IO密集型任務的時候python的多線程能優於單線程。

那有沒有解決辦法呢?當然有,如果是多核處理器可以使用multiprocessing解決這個問題,由於每一個核有單獨的邏輯空間,因此每個核會有一個GIL,這樣就能夠真正意義上充分利用多核的計算資源。

本文參考資料

  • https://www.runoob.com/python3/python3-multithreading.html
  • https://morvanzhou.github.io/tutorials/python-basic/threading/
  • https://www.cnblogs.com/guyuyun/p/11185832.html
  • https://blog.csdn.net/qq_32922423/article/details/83621843
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章