深入理解Python多任務編程----多線程

計算機的設計就是爲了幫助人類或者模仿人類的某些行爲。

生活中的多任務:人可以一邊唱歌🎤一邊跳舞💃、人開車的時候是通過手、腳和眼睛共同配合來駕駛一輛車🚗。

多任務編程就是這樣一個鮮明的例子,計算機也可以實現多任務編程:比如一邊聽歌一邊玩遊戲、打開瀏覽器上網同時能登錄微信、QQ等聊天工具。

那麼Python的多任務有哪些方式呢?

Python多任務編程的三種方式

  • 多線程
  • 多進程
  • 協程

今天我們先來聊一聊Python的多線程編程。

線程

有兩種不同類型的線程:

  • 內核線程
  • 用戶空間線程或用戶線程

內核線程是操作系統的一部分,而用戶空間線程未在內核中實現,關於線程和進程的更多概念請點此處

Python中的線程

Python中有兩個關於線程的模塊:

  • thread
  • threading

Ps:一直以來,thread模塊一直都不被推薦使用,鼓勵推薦使用threading模塊,所以在Python3中的向後兼容,thread模塊被重命名爲_thread

>>> import thread
Traceback (most recent call last):
  File "<pyshell#0>", line 1, in <module>
    import thread
ModuleNotFoundError: No module named 'thread'
>>> import _thread
>>> 

threading 模塊自 Python 1.5.1(1998 年)就已存在,不過有些人仍然繼續使用舊的 thread 模塊。Python 3 把 thread 模塊重命名爲 _thread,以此強調這是低層實現,不應該在應用代碼中使用。

thread模塊

可以使用Thread模塊在單獨的線程中執行功能。爲此,我們可以使用函數thread.start_new_thread

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

此方法可以快速有效地在Linux和Windows中創建新線程。這個方法先接收一個函數對象(或其他可調用對象)和一個參數元組,然後開啓新線程來執行所傳入的函數對象及其傳入的參數。

import _thread


def child(tid):
    print("Hello from thread", tid)


def parent():
    i = 0
    while True:
        i += 1
        _thread.start_new_thread(child, (i,))  # 創建線程的調用
            
        if input() == 'q':
            break
   
            
parent()

我們運行上段程序,然後只要不在控制檯輸入q,就能看到不斷有新的線程創建和退出。當主線程退出時,整個線程就隨之退出了。

Hello from thread 1

Hello from thread 2

Hello from thread 3

Hello from thread 4

Hello from thread 5
q

多線程唱歌跳舞

假如我們讓電腦💻模擬唱跳,就需要啓動兩個線程,同時利用time.sleep避免主線程過早退出,但是線程輸出可能隨機。

from _thread import start_new_thread
import time


def sing():
    for i in range(3):
        print("I'm singing 難忘今宵")
        time.sleep(2)


def dance():
    for i in range(3):
        print("I'm dancing")
        time.sleep(2)


def main():
    start_new_thread(sing, ())
    start_new_thread(dance, ())
    time.sleep(8)
    print('Main thread exiting...')


if __name__ == '__main__':
    main()

如上代碼,我們需要唱3遍“難忘今宵”,同時跳三遍伴舞。time.sleep(8)避免主線程過早退出導致新建的singdance線程提前退出,所以輸出結果可能(每次執行的輸出可能不一樣):

I'm singing 難忘今宵
I'm dancing
I'm dancing
I'm singing 難忘今宵
I'm dancing
I'm singing 難忘今宵
Main thread exiting...

輸出結果的不規律是因爲所有的線程的函數調用都在同一進程中運行,它們共享一個標準輸出流,2個並行運行的線程輸出都混雜在一起了。

更爲重要的是,多個線程訪問共享資源時,必須同步化訪問以避免時間上的重疊。

我們爲了防止主線程退出,整個程序終止,達不到自己想到的效果,利用了sleep()來作爲同步機制,由於這個延時,整個程序的運行時間並沒有比單線程的版本更快,而且多個線程一起共享某個變量/對象,那麼就有可能會丟失其中一個。
我們看一下如下代碼:

from _thread import start_new_thread
import time

num = 0


def plus_one():
    global num
    for i in range(1000):
        num += 1


def minus_one():
    global num
    for i in range(1000):
        num -= 1


def main():
    start_new_thread(plus_one, ())
    start_new_thread(minus_one, ())
    time.sleep(3)
    print(num)


if __name__ == '__main__':
    main()

我們共享一個全局變量num,啓動兩個線程:一個加一1000次,一個減一1000次,最後輸出num的值,好像爲0,但是果真如此嗎?我們是一下循環100000次看看,

from _thread import start_new_thread
import time

num = 0


def plus_one():
    global num
    for i in range(100000):
        num += 1


def minus_one():
    global num
    for i in range(100000):
        num -= 1


def main():
    start_new_thread(plus_one, ())
    start_new_thread(minus_one, ())
    time.sleep(3)
    print(num)


if __name__ == '__main__':
    main()

輸出num結果可能爲整數,也可能爲負數,也可能爲0,這是因爲線程執行順序其實是隨機的。

鎖的概念

正因爲存在上述的問題,所以引出鎖的概念:想要修改一個共享對象,線程需要獲得一把鎖,然後進行修改,之後釋放這把鎖,然後才能被其他線程獲取。通過allocate_lock()創建一個鎖的對象,例如:

from _thread import start_new_thread, allocate_lock
import time

num = 0
mutex = allocate_lock()  # 增加一把鎖


def plus_one():
    global num
    mutex.acquire()  # 獲得鎖

    for i in range(1000000):
        num += 1
    mutex.release()  # 釋放鎖


def minus_one():
    global num
    mutex.acquire()  # 獲得鎖
    for i in range(1000000):
        num -= 1
    mutex.release()  # 釋放鎖


def main():
    start_new_thread(plus_one, ())
    start_new_thread(minus_one, ())
    time.sleep(3)
    print(num)


if __name__ == '__main__':
    main()

這樣執行後結果就會一直是0。

threading模塊

threading是基於對象和類的較高層面上的接口,

threading.Thread((target=function_name, args=(function_parameter1, function_parameterN))

我們也首先實現一個上述加一減一的操作。

import threading

num = 0


def plus_one():
    global num

    for i in range(1000000):
        num += 1


def minus_one():
    global num
    for i in range(1000000):
        num -= 1


def main():
    t1 = threading.Thread(target=plus_one)
    t2 = threading.Thread(target=minus_one)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(num)

if __name__ == '__main__':
    main()

上鎖

import threading

num = 0
mutex = threading.Lock()


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


def minus_one():
    global num
    mutex.acquire()
    for i in range(1000000):
        num -= 1
    mutex.release()


def main():
    t1 = threading.Thread(target=plus_one)
    t2 = threading.Thread(target=minus_one)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    
    print(num)


if __name__ == '__main__':
    main()

到此,我們簡單的介紹了Python中兩個關於線程的模塊,然後通過共享變量引出鎖的概念,不過到此並沒有結束。比如:自定義線程、守護線程、死鎖…

推薦閱讀:

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