Python多線程


Python多線程

  • 模塊:Threading

  • 概念:
    • 線程:CPU執行程序的基本單位
    • 父線程:與子線程而言是相對的。調用別的線程的程序(線程叫做父線程)
    • 子線程:被別的程序所調用,則是調用者的子線程
    • 守護進程:也稱之為后台進程,即主進程(前台進程)結束,守護進程也結束
    • 主線程:最外層的線程(最初運行的程序)
  • 注意:使用Pycharm時,運行多線程程序需要使用Terminal或者Run控制台,而不是Python Console否則得到的將不是原本的多線程結果。(比如守護線程會失效)。參考博客

一下用簡單的幾個例子來講解Python多線程的用法。

第一個例子:展示多線程與普通程序的區別

"""
    最簡單的多線程demo
    展示多線程程序與普通程序的區別
"""

import time
from threading import Thread


def demo1(doing):
    print(f"I want to {doing}, {time.ctime()}")
    time.sleep(2)
    print(f"\n{doing} 結束")

def main():
    print(f"開始, {time.ctime()}")
    t1 = Thread(target=demo1, args=("聽歌",))
    t2 = Thread(target=demo1, args=("吃飯",))
    t1.start()
    t2.start()

    print(f"結束, {time.ctime()}")


if __name__ == '__main__':
    main()
    
"""
開始, Thu May  6 20:22:43 2021
I want to 聽歌, Thu May  6 20:22:43 2021
I want to 吃飯, Thu May  6 20:22:43 2021
結束, Thu May  6 20:22:43 2021
聽歌 結束, Thu May  6 20:22:45 2021
吃飯 結束, Thu May  6 20:22:45 2021
"""

輸出格式經過了略微的調整,原始輸出肯能比較亂,原因之后會講。

由上可以看出,原本需要4秒才能執行完的程序,上面只用了2秒。即實現了並行,同時"聽歌"和"吃飯"。

Python實現多線程使用的是threading庫,使用Thread類創建線程對象,Thread對象有如下比較常用的方法:

  • Thread(target=None, name=None, args=(), daemon=None)
    以上為比較常用的參數,不是所有參數,詳細參數可參考文末的鏈接官網教程
    target 是指明該線程運行的函數名
    name 是我們給該線程取的名稱
    args 是調用該函數需要傳遞的參數元組,末尾加逗號
    daemon 設置該線程是否為守護線程

  • start()
    表示啟動線程,讓其開始工作。該方法每個對象只能夠調用一次,否則拋出RuntimeError,它安排對象的 run()方法在一個獨立的控制進程中調用。

  • run()
    運行target傳遞過去的函數

  • join(timeout=None)
    這個會阻塞調用該線程的線程,如果子線程調用該方法,主線程就算將自己的程序運行完了也不會結束,而是 會等到這個子線程運行完再結束。

  • setName() 和 getName() 分別是設置名字和獲取名字

  • isDaemon() 和 setDaemon() 分別是判斷是否是守護進程和設置守護進程

第二個例子:使用join()方法和將線程模塊化

import threading
import time


class MyThreading(threading.Thread):
    def __init__(self, name):
        super(MyThreading, self).__init__()

        self.name = name

    def run(self):
        print(f"開始線程: {self.name}, {time.ctime()}")
        time.sleep(2)
        print(f"退出線程: {self.name}, {time.ctime()}")


class Test:
    def __init__(self):
        pass

    def main_threading(self):
        print(f"主線程開始運行, {time.ctime()}")
        Thread1 = MyThreading("Thread1")
        Thread2 = MyThreading("Thread2")

        Thread1.start()
        Thread2.start()

        Thread1.join()
        Thread2.join()

        print(f"主線程結束運行, {time.ctime()}")


if __name__ == '__main__':
    test = Test()
    test.main_threading()
    
"""
主線程開始運行, Thu May  6 20:46:30 2021
開始線程: Thread1, Thu May  6 20:46:30 2021
開始線程: Thread2, Thu May  6 20:46:30 2021
退出線程: Thread2, Thu May  6 20:46:32 2021
退出線程: Thread1, Thu May  6 20:46:32 2021
主線程結束運行, Thu May  6 20:46:32 2021
"""

由第一個例子可以知道主線程本應該在子線程結束前結束,但是用了子線程調用join()后會阻塞父線程,致使在子線程結束完后才可結束。

第三個例子:守護線程的使用

以上例子中的子線程結束時間都是根據自身程序的運行時間覺得,而與父線程的是否結束無關,但我們在日常使用時,經常會有父線程結束,子線程就必須結束的一些線程,這種線程我們稱之為守護線程,或者后台進程,如我們的垃圾回收功能就是。

我們修改第二個例子中的Test()類

class Test:
    def __init__(self):
        pass

    def main_threading(self):
        print(f"主線程開始運行, {time.ctime()}")
        Thread1 = MyThreading("Thread1")
        Thread2 = MyThreading("Thread2")
        Thread1.setDaemon(True)
        Thread2.setDaemon(True)
        Thread1.start()
        Thread2.start()

        print(f"主線程結束運行, {time.ctime()}")

"""
    主線程開始運行, Thu May  6 20:48:56 2021
    開始線程: Thread1, Thu May  6 20:48:56 2021
    開始線程: Thread2, Thu May  6 20:48:56 2021
    主線程結束運行, Thu May  6 20:48:56 2021
"""

由運行結果可以看出,父線程結束,子線程就無條件中止了。

第四個例子:

現在的CPU都是多核的,但是python的多線程是無法使用多核,而實現並行的方式是線程切換,只要速度足夠快,在某一段時間內,我們就會認為是並行的。這就是所謂的並發,而某一時刻多個線程同時工作則稱之為並行。我們可以用程序輸出來演示線程切換過程。

import time
from threading import Thread, current_thread


def run(n):
    for i in range(n):
        print(current_thread().name + " " + str(i))


thread = Thread(target=run, args=(20,), name="ChildThread")
# 如果設置子線程為守護進程,可以查看守護進程的結束與主線程的結束時間
# thread.setDaemon(True)
thread.start()

for i in range(10):
    print(current_thread().name + " " + str(i))

"""
ChildThread 0
ChildThread 1
ChildThread 2
ChildThread 3
ChildThread 4
MainThread 0
MainThread 1
MainThread 2
MainThread 3
MainThread 4
MainThread 5
MainThread 6
MainThread 7
ChildThread 5
ChildThread 6
ChildThread 7
ChildThread 8
ChildThread 9
ChildThread 10
ChildThread 11
ChildThread 12
ChildThread 13
MainThread 8
MainThread 9
ChildThread 14
ChildThread 15
ChildThread 16
ChildThread 17
ChildThread 18
ChildThread 19
Process finished with exit code 0
"""

從運行結果我們就可以看出線程的切換。

第五個例子:

由於線程是切換的,那么所以某一個線程在執行時可能被打斷。加上python多線程使用的是同一塊內存,那么資源是共享的,所以當多個線程再某一段時間內,同時訪問某一資源,對其進行修改的話可能會導致混亂。

from threading import Thread

var = 0


def change(n):
    global var
    var += n
    var -= n


def run_thread(n):
    for i in range(1000000):
        change(n)


thread1 = Thread(target=run_thread, args=(5,), name="thread1")
thread2 = Thread(target=run_thread, args=(8,), name="thread2")
thread1.start()
thread2.start()

thread1.join()
thread2.join()

print(var)
"""
11
"""
# 結果不一定

"""
    理論上應該是0,但是當兩個線程交替執行,當循環次數足夠多時,結果就不一定是0
    為了解決這個問題,采用了鎖的辦法
"""

第六個例子:就是解決上述問題的辦法,使用鎖

from threading import Lock, Thread

var = 0
lock = Lock()

def change(n):
    global var
    var += n
    var -= n


def run_thread(n):
    for i in range(20000000):
        lock.acquire()
        try:
            change(n)
        finally:
            lock.release()


thread1 = Thread(target=run_thread, args=(5,), name="thread1")
thread2 = Thread(target=run_thread, args=(8,), name="thread2")
thread1.start()
thread2.start()

thread1.join()
thread2.join()

print(var)

"""
這樣得到的結果就一定是0
"""

可以這么理解這個鎖:人相當於線程,鎖相當於門鎖,一個人進廁所會鎖上門讓別人不能進來打擾,只有當他解鎖出來后別的人才可以進去。

Threading.Lock類的主要方法如下:

  • acquire(blocking=True, timeout=-1)
    • 這個方法用於獲取鎖。獲取的方式可以是阻塞方式,也可以是非阻塞方式
    • 差別就是,如果別的線程使用了鎖,而當前線程想獲取鎖時,可以干等着一直等到使用了鎖的線程釋放了鎖(解鎖),再獲取鎖並鎖定。這樣稱之為阻塞方式
    • 而在非阻塞的情況下(也就是blocking設置為False時),調用acquire方法,若是鎖可獲得,則返回True,並且上鎖,若是鎖被別的線程占用,則返回False。
    • timeout 就是設置阻塞等待的時間,若是在timeout設置的時間內沒有獲得鎖則返回False,否則返回True並獲取鎖,再上鎖。若是timeout=-1,那么將會無限等待。
  • release()
    • 這個方法就是釋放鎖
    • 若是在未鎖定時調用,會返回RuntimeError

Python線程使用場景

由於Python的多線程受到了GIL的原因,Python的多線程無法實現真正的並行,所以多線程程序是通過頻繁的線程切換來實現的。

但是線程切換會帶來不小的開銷,頻繁的線程切換會造成資源的浪費。所以多線程在Python里似乎意味着並沒有帶來性能的提升,甚至降低了程序的性能。

但是有沒有一種操作是正好需要頻繁切換線程才能提高性能的呢?

答案是有的,那就是I/O操作,因為I/O操作不需要一直依靠CPU,只需要CPU向磁盤發起通知,然后就可以去執行其他的命令,等到數據准備好了再讀取數據就好了,就不需要CPU一直等待I/O操作直至完成。

所以Python多線程的使用場景就很明確了:

  • 不適用於CPU計算密集型,而是適用於I/O密集型

以上就是我對python多線程的一些領悟,如有錯誤請積極指出,謝謝。


參考鏈接:廖雪峰的博客阮一峰的博客蟲師的博客官網教程多線程使用場景


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM