Python——多線程以及多線程會產生的一些問題

Python 中的多線程

什麼是線程

​ 一個進程中包括多個線程,線程是 CPU 調度和分派的基本單位,是進程中執行運算的最小單位,真正在 CPU 上運行的是線程,可以與同一個進程中的其他線程共享進程的全部資源

Python 中實現多線程

​ Python 中有兩種方式牀架多線程,一種是調用底層的 thread模塊(Python3 中已棄用),另一種是使用threading模塊,下面我說的也是使用這個模塊實現多線程的方法

​ 從形式上將,多線程的實現和多進程的實現十分類似,threading模塊提供了Thread類來創建線程,同Process類一樣,我們可以通過直接調用或創建子類來繼承這兩種方式來創建線程

使用 Thread 實現多線程

​ 直接調用threading,模塊的Thread類來創建線程十分簡單,使用threading.Thread([target], [(item1, item2, ...)])方法,target爲目標函數名稱(如果有目標函數的話),後邊爲參數元組,用來傳遞函數參數

​ 線程創建好後,調用Thread.start()方法,就可以運行線程,如果沒有目標函數,start()會自動執行Thread類中的run()方法,示例如下:

import threading
import time


def saySorry():
    time.sleep(5)
    print('I am sorry')


if __name__ == '__main__':
    for i in range(5):
        print('create %i Thread' % i)
        t = threading.Thread(target=saySorry)
        t.start()

​ 運行結果:

create 0 Thread
create 1 Thread
create 2 Thread
create 3 Thread
create 4 Thread
I am sorry
I am sorry
I am sorry
I am sorry
I am sorry

​ 上邊的程序也證明了,程序在創建了子線程後,不會等待線程執行完畢,而是會繼續向下執行,而當主程序全部執行完畢後,卻會等待所有子線程執行完畢再結束,這點和進程有些區別

繼承 Thread 實現多線程

​ 另外一種方式是通過創建繼承Thread類的子類來實現多線程,這樣做的好處就是可以將線程要運行的代碼全部放入run函數中,用起來更方便,示例如下:

import threading
import time


class MyThread(threading.Thread):
    def run(self):
        for i in range(3):
            time.sleep(1)
            msg = 'I am ' + self.name + ' @ ' + str(i)
            print(msg)


if __name__ == '__main__':
    t = MyThread()
    t.start()

​ 運行結果:

I am Thread-1 @ 0
I am Thread-1 @ 1
I am Thread-1 @ 2

Python 多線程中全局變量和非全局變量的使用問題

​ 開始就說過,線程可以和同一個進程中的其他線程共享進程的全部資源,那麼在多線程程序中,各線程對全局變量和非全局變量的使用到底是怎樣的呢

非全局變量

​ 非全局變量在多線程中是不會被共享的,這就像是假設有一個存在局部變量a的函數,當程序調用兩次這個函數時,每次調用所產生的局部變量a都是一個新的變量,不會受另一個函數執行的干擾,示例如下:

from threading import Thread
import threading


def test1():
    str = threading.current_thread().name
    g_num = 100
    if str == 'Thread-1':
        print(str)
        g_num += 1
    else:
        print(str)
        g_num -= 1
    print('-----test1----- g_num = %d' % g_num)


p1 = Thread(target=test1)
p1.start()

p2 = Thread(target=test1)
p2.start()

​ 運行結果:

Thread-1
-----test1----- g_num = 101
Thread-2
-----test1----- g_num = 99

​ 可以看到,局部變量g_num並不會因爲另一個線程中的同名函數而收到影響

全局變量

​ 在多線程中,全局變量是可以在各線程間共享的,這也就是說,線程間通信不需要通過管道、內存映射等方法,只需要使用一個全局變量(同一個進程中的共享資源)便可以,示例如下:

from threading import Thread

g_flag = 0
g_num = 0


def test1():
    global g_num
    for i in range(1000000):
        g_num += 1

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


def test2():
    global g_num
    for i in range(1000000):
        g_num += 1

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


p1 = Thread(target=test1)
p1.start()


p2 = Thread(target=test2)
p2.start()

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

​ 運行結果:

---g_num= 372639 ---
---test1--- g_num = 1456098
---test2--- g_num = 1596586 

​ 到這裏,我們可以發現一個問題,正常來講,g_num最後的值不應給是2000000嗎,爲什麼不是呢?這就是多線程使用全局變量時有可能出現的 bug,請繼續閱讀!

Python 多線程中如何防止使用全局變量出現 bug(輪詢和互斥鎖)

​ 通過剛纔在多線程中使用全局變量我們發現,當代碼邏輯稍微複雜一些時,在兩個線程中同時使用一個全局變量會出現問題,是什麼導致了這個問題呢?

​ 從代碼中我們可以發現g_num += 1這句代碼,實際上是g_num + 1和將其結果賦給g_num兩步,正是因爲這連續的兩次對全局變量的操作造成了這個問題

​ 當一個線程執行到g_num + 1這步後,cpu 有可能會轉頭去處理另一個線程,另一個線程也運行了g_num + 1,當 cpu 再回頭執行第一個線程時,g_num 已經不止被運算過一次了

​ 那麼怎麼避免這樣的情況發生呢,只能是如果存在對全局變量變量值的修改時,我們要優先運行一個線程,當它結束修改後,再允許另一個線程去訪問這個局部變量,下面提供兩種方式,輪詢和互斥鎖

輪詢

​ 顧名思義,輪詢的意思就是反覆詢問,抽象起來理解就是,我們可以設置另一個用來作爲目標值的全局變量,兩個線程執行的條件根據目標值的不同而不同,當目標值滿足一個線程執行時,其他線程就會一直處在一個堵塞的過程,它會一直詢問目標值是否符合自己,當上一個線程結束時,這個線程會將目標值修改,這樣下一個符合目標值的線程就會運行,示例如下:

from threading import Thread

g_flag = 0
g_num = 0


def test1():
    global g_num
    global g_flag
    if g_flag == 0:
        for i in range(1000000):
            g_num += 1
        g_flag = 1

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


def test2():
    global g_num
    global g_flag
    while True:
        if g_flag != 0:
            for i in range(1000000):
                g_num += 1
            break

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


p1 = Thread(target=test1)
p1.start()


p2 = Thread(target=test2)
p2.start()

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

​ 運行結果:

---g_num= 181893 ---
---test1--- g_num = 1000000
---test2--- g_num = 2000000 

​ 如結果所示,線程之間使用全局變量的 bug 已經解決,但是輪詢的方法十分消耗資源,堵塞的線程其實一直都處在一個死循環的狀態佔用系統資源

互斥鎖

​ 相比而言,互斥鎖就是一種比較優化的方法,互斥鎖會使用threading模塊的Lock

​ 互斥鎖的思想是,當一個線程運行時,它會給它需要的這部分資源上鎖,這樣同樣使用這把鎖的其他線程全部都會被堵塞,但被互斥鎖所堵塞的線程不會佔用系統資源,它們會處在睡眠狀態,當運行的線程用完被鎖的這部分資源後,它會解鎖,這時其他線程就會被喚醒來搶佔 cpu 資源,得到資源的線程會再次上鎖,達到多線程下全局變量的訪問安全,示例如下:

from threading import Thread
from threading import Lock

g_num = 0
mutex = Lock()


def test1():
    global g_num
    mutex.acquire()
    for i in range(1000000):
        g_num += 1
    mutex.release()

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


def test2():
    global g_num
    mutex.acquire()
    for i in range(1000000):
        g_num += 1
    mutex.release()

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


p1 = Thread(target=test1)
p1.start()

p2 = Thread(target=test2)
p2.start()

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

​ 運行結果:

---g_num= 200009 ---
---test1--- g_num = 1000000
---test2--- g_num = 2000000 

​ 互斥鎖的方法說明:

# 創建鎖
mutex = threading.Lock()
# 上鎖,blocking 爲 True 表示堵塞
mutex.acquire([blocking])
# 解鎖,只要開了鎖,那麼接下來會讓所有因爲這個鎖而被阻塞的線程搶着上鎖
mutex.release()

​ 使用互斥鎖時要注意,爲了提高運算效率,上鎖的資源越少,運算的效率越高

​ 另外,線程等待解鎖的方式不是通過輪詢,二十通過通知,線程會睡眠,等待喚醒的通知,所以互斥鎖較輪詢來講更爲優化

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