1. 程式人生 > >多工(執行緒)

多工(執行緒)

併發:指的是任務數多餘cpu核數,通過作業系統的各種任務排程演算法,實現用多個任務“一起”執行(實際上總有一些任務不在執行,因為切換任務的速度相當快,看上去一起執行而已)

並行:指的是任務數小於等於cpu核數,即任務真的是一起執行的

執行緒:
python的thread模組是比較底層的模組,python的threading模組是對thread做了一些包裝的,可以更加方便的被使用
1. 使用threading模組
單執行緒執行

#coding=utf-8
import time

def saySorry():
    print("親愛的,我錯了,我能吃飯了嗎?")
    time.sleep(1)

if __name__ == "__main__":
    for i in range(5):
        saySorry()

在這裡插入圖片描述
多執行緒執行

#coding=utf-8
import threading
import time

def saySorry():
    print("親愛的,我錯了,我能吃飯了嗎?")
    time.sleep(1)

if __name__ == "__main__":
    for i in range(5):
        t = threading.Thread(target=saySorry)
        t.start() #啟動執行緒,即讓執行緒開始執行

在這裡插入圖片描述
說明

  1. 可以明顯看出使用了多執行緒併發的操作,花費時間要短很多
  2. 當呼叫start()時,才會真正的建立執行緒,並且開始執行
    2. 主執行緒會等待所有的子執行緒結束後才結束
#coding=utf-8
import threading
from time import sleep,ctime

def sing():
    for i in range(3):
        print("正在唱歌...%d"%i)
        sleep(1)

def dance():
    for i in range(3):
        print("正在跳舞...%d"%i)
        sleep(1)

if __name__ == '__main__':
    print('---開始---:%s'%ctime())

    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)

    t1.start()
    t2.start()

    #sleep(5) # 遮蔽此行程式碼,試試看,程式是否會立馬結束?
    print('---結束---:%s'%ctime())

在這裡插入圖片描述
3. 檢視執行緒數量

#coding=utf-8
import threading
from time import sleep,ctime

def sing():
    for i in range(3):
        print("正在唱歌...%d"%i)
        sleep(1)

def dance():
    for i in range(3):
        print("正在跳舞...%d"%i)
        sleep(1)

if __name__ == '__main__':
    print('---開始---:%s'%ctime())

    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)

    t1.start()
    t2.start()

    while True:
        length = len(threading.enumerate())
        print('當前執行的執行緒數為:%d'%length)
        if length<=1:
            break

        sleep(0.5)

在這裡插入圖片描述
python的threading.Thread類有一個run方法,用於定義執行緒的功能函式,可以在自己的執行緒類中覆蓋該方法。而建立自己的執行緒例項後,通過Thread類的start方法,可以啟動該執行緒,交給python虛擬機器進行排程,當該執行緒獲得執行的機會時,就會呼叫run方法執行執行緒。
4. 執行緒的執行順序

#coding=utf-8
import threading
import time

class MyThread(threading.Thread):
    def run(self):
        for i in range(3):
            time.sleep(1)
            msg = "I'm "+self.name+' @ '+str(i)
            print(msg)
def test():
    for i in range(5):
        t = MyThread()
        t.start()
if __name__ == '__main__':
    test()

執行結果:(執行的結果可能不一樣,但是大體是一致的)

  I'm Thread-1 @ 0
    I'm Thread-2 @ 0
    I'm Thread-5 @ 0
    I'm Thread-3 @ 0
    I'm Thread-4 @ 0
    I'm Thread-3 @ 1
    I'm Thread-4 @ 1
    I'm Thread-5 @ 1
    I'm Thread-1 @ 1
    I'm Thread-2 @ 1
    I'm Thread-4 @ 2
    I'm Thread-5 @ 2
    I'm Thread-2 @ 2
    I'm Thread-1 @ 2
    I'm Thread-3 @ 2

說明
從程式碼和執行結果我們可以看出,多執行緒程式的執行順序是不確定的。當執行到sleep語句時,執行緒將被阻塞(Blocked),到sleep結束後,執行緒進入就緒(Runnable)狀態,等待排程。而執行緒排程將自行選擇一個執行緒執行。上面的程式碼中只能保證每個執行緒都執行完整個run函式,但是執行緒的啟動順序、run函式中每次迴圈的執行順序都不能確定。

  1. 每個執行緒預設有一個名字,儘管上面的例子中沒有指定執行緒物件的name,但是python會自動為執行緒指定一個名字。
  2. 當執行緒的run()方法結束時該執行緒完成。
  3. 無法控制執行緒排程程式,但可以通過別的方式來影響執行緒排程的方式。
    5. 多執行緒-共享全域性變數
from threading import Thread
import time

g_num = 100

def work1():
    global g_num
    for i in range(3):
        g_num += 1

    print("----in work1, g_num is %d---"%g_num)


def work2():
    global g_num
    print("----in work2, g_num is %d---"%g_num)


print("---執行緒建立之前g_num is %d---"%g_num)

t1 = Thread(target=work1)
t1.start()

#延時一會,保證t1執行緒中的事情做完
time.sleep(1)

t2 = Thread(target=work2)
t2.start()

結果:

---執行緒建立之前g_num is 100---
----in work1, g_num is 103---
----in work2, g_num is 103---
  1. 在一個程序內的所有執行緒共享全域性變數,很方便在多個執行緒間共享資料
  2. 缺點就是,執行緒是對全域性變數隨意遂改可能造成多執行緒之間對全域性變數的混亂(即執行緒非安全)
    6多執行緒-共享全域性變數問題
    假設兩個執行緒t1和t2都要對全域性變數g_num(預設是0)進行加1運算,t1和t2都各對g_num加10次,g_num的最終的結果應該為20。

但是由於是多執行緒同時操作,有可能出現下面情況:

  1. 在g_num=0時,t1取得g_num=0。此時系統把t1排程為”sleeping”狀態,把t2轉換為”running”狀態,t2也獲得g_num=0
  2. 然後t2對得到的值進行加1並賦給g_num,使得g_num=1
  3. 然後系統又把t2排程為”sleeping”,把t1轉為”running”。執行緒t1又把它之前得到的0加1後賦值給g_num。
  4. 這樣導致雖然t1和t2都對g_num加1,但結果仍然是g_num=1
    測試1:
import threading
import time

g_num = 0

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


def work2(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("----in work2, g_num is %d---"%g_num)


print("---執行緒建立之前g_num is %d---"%g_num)

t1 = threading.Thread(target=work1, args=(100,))
t1.start()

t2 = threading.Thread(target=work2, args=(100,))
t2.start()

while len(threading.enumerate()) != 1:
    time.sleep(1)

print("2個執行緒對同一個全域性變數操作之後的最終結果是:%s" % g_num)

結果:

---執行緒建立之前g_num is 0---
----in work1, g_num is 100---
----in work2, g_num is 200---
2個執行緒對同一個全域性變數操作之後的最終結果是:200

測試2:

import threading
import time

g_num = 0

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


def work2(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("----in work2, g_num is %d---"%g_num)


print("---執行緒建立之前g_num is %d---"%g_num)

t1 = threading.Thread(target=work1, args=(1000000,))
t1.start()

t2 = threading.Thread(target=work2, args=(1000000,))
t2.start()

while len(threading.enumerate()) != 1:
    time.sleep(1)

print("2個執行緒對同一個全域性變數操作之後的最終結果是:%s" % g_num)

結果:

---執行緒建立之前g_num is 0---
----in work1, g_num is 1088005---
----in work2, g_num is 1286202---
2個執行緒對同一個全域性變數操作之後的最終結果是:1286202

如果多個執行緒同時對同一個全域性變數操作,會出現資源競爭問題,從而資料結果會不正確

解決執行緒同時修改全域性變數的方式
對於那個計算錯誤的問題,可以通過執行緒同步來進行解決

思路,如下:

  1. 系統呼叫t1,然後獲取到g_num的值為0,此時上一把鎖,即不允許其他執行緒操作g_num
  2. t1對g_num的值進行+1
  3. t1解鎖,此時g_num的值為1,其他的執行緒就可以使用g_num了,而且是g_num的值不是0而是1
  4. 同理其他執行緒在對g_num進行修改時,都要先上鎖,處理完後再解鎖,在上鎖的整個過程中不允許其他執行緒訪問,就保證了資料的正確性

互斥鎖
當多個執行緒幾乎同時修改某一個共享資料的時候,需要進行同步控制

執行緒同步能夠保證多個執行緒安全訪問競爭資源,最簡單的同步機制是引入互斥鎖。

互斥鎖為資源引入一個狀態:鎖定/非鎖定

某個執行緒要更改共享資料時,先將其鎖定,此時資源的狀態為“鎖定”,其他執行緒不能更改;直到該執行緒釋放資源,將資源的狀態變成“非鎖定”,其他的執行緒才能再次鎖定該資源。互斥鎖保證了每次只有一個執行緒進行寫入操作,從而保證了多執行緒情況下資料的正確性。

threading模組中定義了Lock類,可以方便的處理鎖定:

# 建立鎖
mutex = threading.Lock()

# 鎖定
mutex.acquire()

# 釋放
mutex.release()

注意:

  • 果這個鎖之前是沒有上鎖的,那麼acquire不會堵塞
  • 如果在呼叫acquire對這個鎖上鎖之前 它已經被 其他執行緒上了鎖,那麼此時acquire會堵塞,直到這個鎖被解鎖為止

使用互斥鎖完成2個執行緒對同一個全域性變數各加100萬次的操作

import threading
import time

g_num = 0

def test1(num):
    global g_num
    for i in range(num):
        mutex.acquire()  # 上鎖
        g_num += 1
        mutex.release()  # 解鎖

    print("---test1---g_num=%d"%g_num)

def test2(num):
    global g_num
    for i in range(num):
        mutex.acquire()  # 上鎖
        g_num += 1
        mutex.release()  # 解鎖

    print("---test2---g_num=%d"%g_num)

# 建立一個互斥鎖
# 預設是未上鎖的狀態
mutex = threading.Lock()

# 建立2個執行緒,讓他們各自對g_num加1000000次
p1 = threading.Thread(target=test1, args=(1000000,))
p1.start()

p2 = threading.Thread(target=test2, args=(1000000,))
p2.start()

# 等待計算完成
while len(threading.enumerate()) != 1:
    time.sleep(1)

print("2個執行緒對同一個全域性變數操作之後的最終結果是:%s" % g_num)

結果:

---test1---g_num=1909909
---test2---g_num=2000000
2個執行緒對同一個全域性變數操作之後的最終結果是:2000000

上鎖解鎖過程
當一個執行緒呼叫鎖的acquire()方法獲得鎖時,鎖就進入“locked”狀態。

每次只有一個執行緒可以獲得鎖。如果此時另一個執行緒試圖獲得這個鎖,該執行緒就會變為“blocked”狀態,稱為“阻塞”,直到擁有鎖的執行緒呼叫鎖的release()方法釋放鎖之後,鎖進入“unlocked”狀態。

執行緒排程程式從處於同步阻塞狀態的執行緒中選擇一個來獲得鎖,並使得該執行緒進入執行(running)狀態。

鎖的好處:

  • 確保了某段關鍵程式碼只能由一個執行緒從頭到尾完整地執行

鎖的壞處:

  • 阻止了多執行緒併發執行,包含鎖的某段程式碼實際上只能以單執行緒模式執行,效率就大大地下降了
  • 由於可以存在多個鎖,不同的執行緒持有不同的鎖,並試圖獲取對方持有的鎖時,可能會造成死鎖