我的憨憨女友都能看懂學會的python多線程

我和我的女朋友因爲python而相識,同時也是因爲python我才能把憨憨追到手。最近我和我女朋友在做一個項目,我負責語音識別和TTS,她負責QT界面設計。終於在上一個周我們都完成了各自預期的功能。到了兩個代碼整合的階段,卻發現了一個難題:怎麼樣才能實現語音和界面同時工作,同時怎麼樣才能保證通過語音來打開相關的界面,以及在視頻通話時語音不工作,這些問題讓我倆抓狂。看看我女朋友的頭髮最近掉的厲害,作爲一個男人我必須扛起責任!於是我攔下這活,並且給我女朋友說道:等我學會了python多線程我講給你聽!
Alt

線程和進程

計算機的核心是CPU,它承擔了所有的計算任務,就像是一座工廠在時刻運行
在這裏插入圖片描述
如果工廠的資源有限,一次只能供一個車間來使用,也就是說當一個車間開工時其它車間不能工作,也就是一個CPU一次只能執行一個任務。

  • 進程就好比工廠的車間,它代表CPU所能處理的單個任務。任一時刻,CPU總是運行一個進程,其他進程處於非運行狀態。

當然一個車間還有很多工人,他們互相協同完成一個工作
Alt

  • 而線程就好比工廠的工人,一個進程可以包含多個線程

線程(Thread)也叫輕量級進程,是操作系統能夠進行運算調度的最小單位,它被包涵在進程之中,是進程中的實際運作單位。線程自己不擁有系統資源,只擁有一點兒在運行中必不可少的資源,但它可與同屬一個進程的其它線程共享進程所擁有的全部資源。一個線程可以創建和撤消另一個線程,同一進程中的多個線程之間可以併發執行

多線程與多進程

通俗易懂的理解就是:

多進程:允許多個任務同時進行
多線程:允許單個任務分成不同的部分運行

python多線程的實現

Python3 通過兩個標準庫 thread (python2中是thread模塊)和 threading 提供對線程的支持。
thread 提供了低級別的、原始的線程以及一個簡單的鎖,它相比於 threading 模塊的功能還是比較有限的。

threading

import threading #導入threading庫
import time

def run(n):
    print("task", n)
    time.sleep(1) #延時一秒
    print('2s')
    time.sleep(1)
    print('1s')
    time.sleep(1)
    print('0s')
    time.sleep(1)

if __name__ == '__main__':
    t1 = threading.Thread(target=run, args=("t1",))#創建線程1,取名爲t1
    t2 = threading.Thread(target=run, args=("t2",))#創建線程2,取名爲t2
    t1.start() #開啓線程t1
    t2.start() #開啓線程t2

輸出結果:

task t1
task t2
2s
2s
1s
1s
0s
0s

可以看出先開啓了線程t1,在開啓t2然後每隔一秒打印數據

自定義線程

通過繼承threading.Thread來自定義線程類,其本質重構Thread類中的run方法

import threading
import time

class MyThread(threading.Thread):
    def __init__(self, n):
        super(MyThread, self).__init__()  # 重構run函數必須要寫
        self.n = n

    def run(self):
        print("task", self.n)
        time.sleep(1)
        print('2s')
        time.sleep(1)
        print('1s')
        time.sleep(1)
        print('0s')
        time.sleep(1)

if __name__ == "__main__":
    t1 = MyThread("t1")
    t2 = MyThread("t2")
    t1.start()
    t2.start()

輸出結果:

task t1
task t2
2s
2s
1s
1s
0s
0s

守護線程

下面這個例子,使用setDaemon(True)把所有的子線程都變成了主線程的守護線程,因此當主進程結束後,子線程也會隨之結束。所以當主線程結束後,整個程序就退出了

import threading
import time

def run(n):
    print("task", n)
    time.sleep(1)       #此時子線程停1s
    print('3')
    time.sleep(1)
    print('2')
    time.sleep(1)
    print('1')

if __name__ == '__main__':
    t = threading.Thread(target=run, args=("t1",))
    t.setDaemon(True)   #把子進程設置爲守護線程,必須在start()之前設置
    t.start()
    print("end")

輸出結果:

task t1
end

可以看到,t1線程並沒有執行完畢,而是直接結束了。說明設置子線程爲守護線程之後,主線程結束了,子線程也立即結束不再執行。

程序中不是隻創建了一個線程麼?怎麼會有主線程和子線程呢?
在這裏插入圖片描述
其實呢程序運行時就會創建一個線程,而這個線程就是主線程

主線程等待子線程運行結束

import threading
import time

def run(n):
    print("task", n)
    time.sleep(1)      
    print('3')
    time.sleep(1)
    print('2')
    time.sleep(1)
    print('1')

if __name__ == '__main__':
    t = threading.Thread(target=run, args=("t1",))
    t.setDaemon(True)   #把子進程設置爲守護線程,必須在start()之前設置
    t.start()
    t.join() # 設置主線程等待子線程結束
    print("end")

輸出結果:

task t1
3
2
1
end

運行.join()後的程序表明等待所有線程結束以後再進行.join()之後的操作結合以上代碼就是,等待t1結束以後再執行end

多線程共享全局變量

線程是進程的執行單元,進程是系統分配資源的最小單位,所以在同一個進程中的多線程是共享資源的。那麼共享資源時就需要用到全局變量。

import threading
import time

num = 100

def work1():
    global num
    for i in range(3):
        num += 1
    print("in work1 num is : %d" % num)

def work2():
    global num
    print("in work2 num is : %d" % num)

if __name__ == '__main__':
    t1 = threading.Thread(target=work1)
    t1.start()
    time.sleep(1)
    t2 = threading.Thread(target=work2)
    t2.start()

運行結果如下:

in work1 num is : 103
in work2 num is : 103

可以看到兩者輸出的結果是相同的,說明是可以共享全局變量的。

互斥鎖

由於線程之間是進行隨機調度,並且每個線程可能只執行n條,當多個線程同時修改同一條數據時可能會出現髒數據,因而,出現了線程鎖,即同一時刻只允許一個線程執行操作。線程鎖用於鎖定資源,可以定義多個鎖, 在下面的實例中, 當你需要獨佔某一資源時,任何一個鎖都可以鎖這個資源,就好比你用不同的鎖都可以把相同的一個門鎖住是一個道理。

由於線程之間是進行隨機調度,如果有多個線程同時操作一個對象,如果沒有很好地保護該對象,會造成程序結果的不可預期,我們也稱此爲“線程不安全”。

爲了方式上面情況的發生,就出現了互斥鎖(Lock)

import threading

def work1():
	global A,lock#定義A和lock爲全局變量
	lock.acquire()#上鎖
	for i in range(5):
		A+=1
		print('work1',A)
	lock.release()#解鎖
def work2():
	global A,lock
	lock.acquire()
	for i in range(5):
		A+=10
		print('work2',A)
	lock.release()
if __name__=='__main__':
	lock=threading.Lock()#定義鎖
	A=0
	t1=threading.Thread(target=work1)
	t2=threading.Thread(target=work2)
	t1.start()
	t2.start()
	t1.join()
	t2.join()

輸出結果:

work1 1
work1 2
work1 3
work1 4
work1 5
work2 15
work2 25
work2 35
work2 45
work2 55

可以發現對兩組數據是沒有影響的,感興趣的可以嘗試一下不加鎖會有什麼情況。

遞歸鎖

RLcok類的用法和Lock類一模一樣,但它支持嵌套,在多個鎖沒有釋放的時候一般會使用RLcok類。

import threading
import time

def Func(lock):
    global gl_num
    lock.acquire()
    gl_num += 1
    time.sleep(1)
    print(gl_num)
    lock.release()

if __name__ == '__main__':
    gl_num = 0
    lock = threading.RLock()
    for i in range(10):
        t = threading.Thread(target=Func, args=(lock,))
        t.start()

輸出結果:

1
2
3
4
5
6
7
8
9
10

信號量(BoundedSemaphore類)

互斥鎖同時只允許一個線程更改數據,而Semaphore是同時允許一定數量的線程更改數據 ,比如廁所有3個坑,那最多隻允許3個人上廁所,後面的人只能等裏面有人出來了才能再進去。
實際中博主還沒有用到過,所以理解不是特別透徹。

import threading
import time

def run(n, semaphore):
    semaphore.acquire()   #加鎖
    time.sleep(1)
    print("run the thread:%s\n" % n)
    semaphore.release()     #釋放

if __name__ == '__main__':
    num = 0
    semaphore = threading.BoundedSemaphore(5)  # 最多允許5個線程同時運行
    for i in range(22):
        t = threading.Thread(target=run, args=("t-%s" % i, semaphore))
        t.start()
    while threading.active_count() != 1:
        pass  # print threading.active_count()
    else:
        print('-----all threads done-----')

輸出結果有點長,就不貼輸出結果了。

事件(Event類)

python線程的事件用於主線程控制其他線程的執行,事件是一個簡單的線程同步對象,其主要提供以下幾個方法:

  • clear 將flag設置爲“False”
  • set 將flag設置爲“True”
  • is_set 判斷是否設置了flag
  • wait 會一直監聽flag,如果沒有檢測到flag就一直處於阻塞狀態

事件處理的機制:全局定義了一個“Flag”,當flag值爲“False”,那麼event.wait()就會阻塞,當flag值爲“True”,那麼event.wait()便不再阻塞

import threading
import time
event = threading.Event()
def lighter():
    count = 0
    event.set()     #初始值爲綠燈
    while True:
        if 5 < count <=10 :
            event.clear()  # 紅燈,清除標誌位
            print("1mred light is on...")
        elif count > 10:
            event.set()  # 綠燈,設置標誌位
            count = 0
        else:
            print("mgreen light is on...")
        time.sleep(1)
        count += 1
def car(name):
    while True:
        if event.is_set():      #判斷是否設置了標誌位
            print("[%s] running..."%name)
            time.sleep(1)
        else:
            print("[%s] sees red light,waiting..."%name)
            event.wait()
            print("[%s] green light is on,start going..."%name)
light = threading.Thread(target=lighter,)
light.start()

car = threading.Thread(target=car,args=("MINI",))
car.start()

這段代碼模擬紅綠燈,很形象。

Qthread

本以爲我學完了多線程就完事了,就可以將語音和QT界面進行整合了。當我去實現的時候發現問題不是這麼簡單,通過語音控制打開一個特定的界面可以實現,但是爲什麼只要這個特定的界面關閉了,我語音的線程也就結束了。
在這裏插入圖片描述

困惑了我好久,最後終於在某社區發現了答案!原來QT自帶的有Qthread,當多線程涉及到界面交互時最好用Qthread來實現。然後又查閱大量博客,看了大量代碼
在使用繼承QThread的run方法之前需要了解一條規則:

QThread只有run函數是在新線程裏的,其他所有函數都在QThread生成的線程裏

QThread只有run函數是在新線程裏的
QThread只有run函數是在新線程裏的
QThread只有run函數是在新線程裏的

方法 描述
start() 啓動線程
wait() 阻塞線程,直到滿足如下條件之一:1. 與此QThread對象關聯的線程完成執行,此函數將返回True;如果線程尚未啓動,此函數也返回True。 2. 等待時間的單位是毫秒。如果時間是ULONG_MAX(默認值),則等待,永遠不會超時;如果等待超時,則返回False
started() 開始執行run()之前,與相關線程發射此信號
finished() 當程序完成任務時,發射此信號
sleep() 強制線程休眠(單位:秒)

那麼我就在網上找到了這個計時器的例子:

#coding=utf-8
import sys

from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *

count = 0


# 工作線程
class WorkThread(QThread):
    # pyqtSignal是信號類
    timeout = pyqtSignal()  # 每隔一秒發送一個信號
    end = pyqtSignal()  # 計數完成後發送一個信號

    def run(self):
        while True:
            # 休眠1秒
            self.sleep(1)
            if count == 5:
                self.end.emit()  # 發送end信號,調用和end信號關聯的方法
                break
            self.timeout.emit()  # 發送timeout信號


class Counter(QWidget):
    def __init__(self):
        super(Counter, self).__init__()

        self.setWindowTitle("用QThread編寫計數器")
        self.resize(600, 400)

        layout = QVBoxLayout()

        # QLCDNumber 用於模擬LED顯示效果,類似於Label
        self.lcdNumber = QLCDNumber()
        layout.addWidget(self.lcdNumber)

        button = QPushButton("開始計數")
        layout.addWidget(button)

        self.workThread = WorkThread()
        self.workThread.timeout.connect(self.countTime)
        self.workThread.end.connect(self.end)
        button.clicked.connect(self.work)

        self.setLayout(layout)

    def countTime(self):
        global count
        count += 1
        self.lcdNumber.display(count)

    def end(self):
        QMessageBox.information(self, '消息', '計數結束', QMessageBox.Ok)
        global count
        count =0

    def work(self):
        self.workThread.start()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    main = Counter()
    main.show()
    sys.exit(app.exec_())

點擊開始計時就會出現類似LCD的顯示,計時到5秒結束後彈窗提醒。
運行結果如下:
Alt
在這裏插入圖片描述
通過這個例程讓我對Qthread有了更好的理解,經管理解的不是特別透徹但是我知道怎麼來改出來我想用的代碼。之前提到的打開窗口線程阻塞,關閉窗口線程重啓,其實這個計時器是一個很好的例子,但是關於線程阻塞.wait不好使。我的方法是定義一個全局變量mode=0(用來判斷是否需要阻塞線程),如果窗口打開後那麼給這個全局賦值mode=1,在run函數裏對這個mode進行判斷,如果mode等於1那麼可以用一個循環來延時實現。

if mode:
	while(mode):
		self.sleep(1)

窗口關閉以後給mode 賦值等於0通過這種方法可以實現,很多小夥伴又會問怎麼判斷窗口打開和關閉,其實在自己寫的窗口函數最前面加mode=1和最後面mode=0就可以了不用進行判斷。

在看完這篇文章後,我女朋友終於給我發來了下面的表情,對我投來羨慕的眼神
Alt

在這裏插入圖片描述

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