(十五)Python中級知識-線程模塊

1、概述

在Python3中主要有3個線程模塊,即:_thread、threading、queue模塊;
_thread模塊:在 3.7 版進行了更改,這個模塊曾經是可選的,但現在總是可用的,之前叫thread。
_thread模塊:提供了操作多個線程(也被稱爲 輕量級進程 或 任務)的底層原語 —— 多個控制線程共享全局數據空間。爲了處理同步問題,也提供了簡單的鎖機制(也稱爲 互斥鎖 或 二進制信號)。
threading 模塊:這個模塊在較低級的模塊 _thread 基礎上建立較高級的線程接口。
queue模塊:實現了多生產者、多消費者隊列。這特別適用於消息必須安全地在多線程間交換的線程編程。模塊中的 Queue 類實現了所有所需的鎖定語義。

本文案例在python3.8環境下運行。

2、_thread模塊應用

使用這個模塊之前,先導入它:

import _thread

如何來創建一個線程?
通過文檔我們找到一個函數:

_thread.start_new_thread(function, args[, kwargs])

函數解釋:
開啓一個新線程並返回其標識。
function :線程執行函數;
args :附帶參數列表 (必須是元組)。
kwargs :可選的 ,參數指定一個關鍵字參數字典。
當函數返回時,線程會靜默地退出。

這與java類似,線程內語句執行完,線程會自動結束並退出。
當函數因某個未處理異常而終結時,sys.unraisablehook() 會被調用以處理異常。
鉤子參數的 object 屬性爲 function。 在默認情況下,會打印堆棧回溯然後該線程將退出(但其他線程會繼續運行)。
當函數引發 SystemExit 異常時,它會被靜默地忽略。

舉個例子:

import _thread
import time


# 自定義的線程內調用的函數
def show(threadName, delaySec, times):
    while times > 0:
        time.sleep(delaySec)
        times -= 1
        print("%s: %s" % (threadName, time.ctime(time.time())))


_thread.start_new_thread(show, ("Thread-1", 1, 5))
_thread.start_new_thread(show, ("Thread-2", 1, 4))

time.sleep(10)  # 讓主線程休眠10秒,足夠子線程完成

當我們在主線程中使用start_new_thread創建新的線程的時,主線程無法得子知線程何時結束,導致主線程已經執行完了,子線程卻還未完成,因此會使用這句time.sleep(10) 。

輸出結果:

Thread-1: Sat Mar 21 10:52:35 2020
Thread-1: Sat Mar 21 10:52:35 2020
Thread-2: Sat Mar 21 10:52:35 2020
Thread-1: Sat Mar 21 10:52:36 2020
Thread-1: Sat Mar 21 10:52:36 2020
Thread-2: Sat Mar 21 10:52:36 2020
Thread-2: Sat Mar 21 10:52:37 2020
Thread-2: Sat Mar 21 10:52:38 2020

可以看到Thread-1和Thread-2交替打印,每次運行的結果都會有很大概率的不同。

前面例子提到 time.sleep(10) 使用主線成延時來等待子線程執行完成。但是有一些缺陷,我們需要大概估計子線程執行的時間然後設置延時,萬一我們執行的子線程耗時不可估計,那麼這時候使用延時這種方式就不太準確,因此我們採用另外一種方式,引入鎖的概念,就是保證讓子線程執行前加鎖,執行完成釋放鎖,主線程根據鎖的狀態判斷子線程是否執行完成來決定是否退出程序。

舉個例子:
import _thread
import time


# 自定義的線程內調用的函數
def show(threadName, delaySec, times, lock):
    while times > 0:
        time.sleep(delaySec)
        times -= 1
        print("%s: %s" % (threadName, time.ctime(time.time())))
    # 釋放鎖
    lock.release()


myLock = _thread.allocate_lock()  # 獲取locktype對象
# 獲取鎖 
myLock.acquire(1, 1)

_thread.start_new_thread(show, ("Thread-1", 0.5, 4, myLock))
# 可以試試放開下面這句話,同時運行兩個線程,會導致輸出結果並不全,解決這個問題我們可以使用threading模塊
# _thread.start_new_thread(show, ("Thread-2", 0.5, 4, myLock))

# 獲取鎖的狀態,當鎖一直被子線程佔用時,程序處於等待狀態
while myLock.locked():
    pass

輸出結果:
Thread-1: Sat Mar 21 11:29:53 2020
Thread-1: Sat Mar 21 11:29:54 2020
Thread-1: Sat Mar 21 11:29:54 2020
Thread-1: Sat Mar 21 11:29:55 2020

Process finished with exit code 0

如果創建兩個及以上的線程會導致輸出結果不是預期的結果,要解決這個問題我們需要引入threading模塊來解決這個問題

3、threading模塊應用

使用這個模塊之前,先導入它:

import threading

創建並啓動線程:

舉個例子:
import threading
import time


# 通過繼承實現一個自己的線程類,類似java 的runnable
class MyThread(threading.Thread):
    def __init__(self, threadId, threadName, delaySec, times):
        threading.Thread.__init__(self)
        self.threadId = threadId
        self.threadName = threadName
        self.delaySec = delaySec
        self.times = times

    # 重載threading.Thread 的run 方法,類似 java 實現 runnable 後實現run 方法
    def run(self):
        print("線程%s:run->開始" % (self.threadName))
        show(self.threadId, self.threadName, self.delaySec, self.times)
        print("線程%s:run->結束" % (self.threadName))


# 自定義的線程內調用的函數
def show(threadId, threadName, delaySec, times):
    while times > 0:
        time.sleep(delaySec)
        times -= 1
        print("%s: %s: %s" % (threadId, threadName, time.ctime(time.time())))


myThread = MyThread(1, "Thread-1", 0.5, 5)
myThread2 = MyThread(2, "Thread-2", 1, 5)
myThread.start()
myThread2.start()
# 將兩個子線程加入主線程,等待子線程中止後再退出主線程。
# 應用場景:主線程生成並啓動了子線程,而子線程裏要進行大量的耗時的運算(這裏可以借鑑下線程的作用),當主線程處理完其他的事務後,需要用到子線程的處理結果,這個時候就要用到join();方法了。
myThread.join()
myThread2.join()

print ("退出主線程")

例子中使用了join函數,什麼時候使用它?

應用場景:主線程生成並啓動了子線程,而子線程裏要進行大量的耗時的運算(這裏可以借鑑下線程的作用),當主線程處理完其他的事務後,需要用到子線程的處理結果,這個時候就要用到join()方法。

主線程:即每一個應用程序啓動後都會創建一個主線程,在這個主線程中再創建的線程都叫子線程。

輸出結果:
線程Thread-1:run->開始
線程Thread-2:run->開始
1: Thread-1: Sat Mar 21 12:03:17 2020
2: Thread-2: Sat Mar 21 12:03:18 2020
1: Thread-1: Sat Mar 21 12:03:18 2020
1: Thread-1: Sat Mar 21 12:03:18 2020
2: Thread-2: Sat Mar 21 12:03:19 2020
1: Thread-1: Sat Mar 21 12:03:19 2020
1: Thread-1: Sat Mar 21 12:03:19 2020
線程Thread-1:run->結束
2: Thread-2: Sat Mar 21 12:03:20 2020
2: Thread-2: Sat Mar 21 12:03:21 2020
2: Thread-2: Sat Mar 21 12:03:22 2020
線程Thread-2:run->結束
退出主線程

Process finished with exit code 0

可以看出線程一和線程而隨機交替執行。
接下來我們需要解決上一節我們提到的,_thread模塊在兩個及以上的情況下,準確判斷子線程是否都執行完,如果執行完則退出程序:

舉個例子:
import threading
import time


# 實現一個自己的線程類,類似java 的runnable
class MyThread(threading.Thread):
    def __init__(self, threadId, threadName, delaySec, times):
        threading.Thread.__init__(self)
        self.threadId = threadId
        self.threadName = threadName
        self.delaySec = delaySec
        self.times = times

    # 重載threading.Thread 的run 方法,類似 java 實現 runnable 後實現run 方法
    def run(self):
        print("線程%s:run->開始" % (self.threadName))
        # 獲取鎖
        threadLock.acquire()
        show(self.threadId, self.threadName, self.delaySec, self.times)
        # 釋放鎖,開啓下一個線程
        threadLock.release()
        print("線程%s:run->結束" % (self.threadName))


# 自定義的線程內調用的函數
def show(threadId, threadName, delaySec, times):
    while times > 0:
        time.sleep(delaySec)
        times -= 1
        print("%s: %s: %s" % (threadId, threadName, time.ctime(time.time())))


# 獲取線程鎖對象
threadLock = threading.Lock()
threads = []

myThread = MyThread(1, "Thread-1", 0.5, 5)
myThread2 = MyThread(2, "Thread-2", 1, 5)

threads.append(myThread)
threads.append(myThread2)

myThread.start()
myThread2.start()

# 等待所有線程完成
for t in threads:
    t.join()

print("退出主線程")

輸出結果:
線程Thread-1:run->開始
線程Thread-2:run->開始
1: Thread-1: Sat Mar 21 12:11:35 2020
1: Thread-1: Sat Mar 21 12:11:36 2020
1: Thread-1: Sat Mar 21 12:11:36 2020
1: Thread-1: Sat Mar 21 12:11:37 2020
1: Thread-1: Sat Mar 21 12:11:37 2020
線程Thread-1:run->結束
2: Thread-2: Sat Mar 21 12:11:38 2020
2: Thread-2: Sat Mar 21 12:11:39 2020
2: Thread-2: Sat Mar 21 12:11:40 2020
2: Thread-2: Sat Mar 21 12:11:41 2020
2: Thread-2: Sat Mar 21 12:11:42 2020
線程Thread-2:run->結束
退出主線程

Process finished with exit code 0

通過這個輸出結果我們看到了線程同步的效果,且保證子線程執行完後結束主線程。

特別提示:如果我們不想實現同步效果,繼續保持異步,則只需要註釋threadLock這個鎖變量相關的3行代碼。

4、queue模塊應用

線程優隊列
使用這個模塊之前,先導入它:

import queue

queue模塊實現了三種類型的隊列,它們的區別僅僅是條目取回的順序。

  • FIFO 隊列:先添加的任務先取回;
  • LIFO 隊列:最近被添加的條目先取回(操作類似一個堆棧);
  • 優先級隊:條目將保持排序( 使用 heapq 模塊 ) 並且最小值的條目第一個返回。
舉一個優先級隊列的例子:
import queue
import threading
import time

exitFlag = 0


# 實現一個自己的線程類,類似java 的runnable
class MyThread(threading.Thread):
    def __init__(self, threadId, threadName,  que):
        threading.Thread.__init__(self)
        self.threadId = threadId
        self.threadName = threadName
        self.que = que

    # 重載threading.Thread 的run 方法,類似 java 實現 runnable 後實現run 方法
    def run(self):
        print("線程%s:run->開始" % (self.threadName))
        show(self.threadId, self.threadName,  self.que)
        print("線程%s:run->結束" % (self.threadName))


# 自定義的線程內調用的函數
def show(threadId, threadName,  que):
    while not exitFlag:
        queueLock.acquire()
        if not workQueue.empty():
            data = que.get()
            queueLock.release()
            print("%s: %s: %s,傳遞的數據:%s" % (threadId, threadName, time.ctime(time.time()), data))
        else:
            queueLock.release()


threadNameList = ["Thread-1", "Thread-2", "Thread-3", "Thread-4"]
nameList = ["One1", "Two2", "Three3", "Four4",'Five5','Six6','Seven7']
# 獲取線程鎖對象
queueLock = threading.Lock()
# 創建10個線程的隊列空間
workQueue = queue.Queue(10)
threads = []
threadId = 1;

for threadName in threadNameList:
    myThread = MyThread(threadId, threadName, workQueue)
    myThread.start()
    threads.append(myThread)
    threadId += 1

queueLock.acquire()
for name in nameList:
    workQueue.put(name)
queueLock.release()

# 等待隊列清空
while not workQueue.empty():
    pass

# 通知線程是時候退出
exitFlag = 1

# 等待所有線程完成
for t in threads:
    t.join()

print("退出主線程")

輸出結果:
線程Thread-1:run->開始
線程Thread-2:run->開始
線程Thread-3:run->開始
線程Thread-4:run->開始
2: Thread-2: Sat Mar 21 14:11:11 2020,傳遞的數據:One1
3: Thread-3: Sat Mar 21 14:11:11 2020,傳遞的數據:Two2
3: Thread-3: Sat Mar 21 14:11:11 2020,傳遞的數據:Three3
3: Thread-3: Sat Mar 21 14:11:11 2020,傳遞的數據:Four4
3: Thread-3: Sat Mar 21 14:11:11 2020,傳遞的數據:Five5
3: Thread-3: Sat Mar 21 14:11:11 2020,傳遞的數據:Six6
3: Thread-3: Sat Mar 21 14:11:11 2020,傳遞的數據:Seven7
線程Thread-3:run->結束
線程Thread-1:run->結束
線程Thread-4:run->結束線程Thread-2:run->結束

退出主線程

Process finished with exit code 0

例子創建了四個子線程輸出,使用的隊列容量爲10,將nameList 數據放入隊列,按照放入的順序輸出,並且nameList[0]第一個值先返回。

5、總結:

_thread不太推薦使用了,尤其是在多線程的時候,會出現預期的結果不正確。
推薦使用threading模塊,處理多線程問題會得到我們想要的預期結果。
join() 這個函數是保證子線程執行完之後再退出主線程,防止內存泄漏或主線程執行完了子線程還未執行完的情況。
在調用.acquire()和.release()兩個函數時IDE沒有代碼自動提示,比較奇怪。

參考鏈接:queue模塊
參考鏈接:_thread模塊
參考鏈接:threading模塊

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章