天天看點

Python基礎 - 多線程(上)

基于再多程序基礎下, 認識多線程, 即線程是程序的基本單元來了解

前面對 程序 一點認識, 通俗了解, 程序是作業系統(OS)進行資源排程配置設定的基本單元. 每個程式的至少就一個程序在OS中被"監控"着的哦. 然後圍繞着多程序, 用消息隊列共享全局變量, 守護主程序, 程序池...這一通探讨, 當然還是偏向應用的一方, 我自己偶爾工作有多任務的處理的地方, 也是優先弄多程序 (主要是公司電腦賊強, 我就要弄多程序, 就要浪費資源哈哈..).

程序 呢, 基本沒用過, (爬蟲除外, 之前有用 scrapy 是多線程的), 自己來手寫應該是沒有的. 為啥甯願優先考慮程序呢, 原因是我們大多用的 Python 的解釋是 CPython. 它預設下, 其實是有設阻塞的, 在多線程一塊, 其實 CPython 沒有真正地能實作 多線程.. 先不說這...

也是從通俗的角度來了解的話, 一個程序(程式), 至少有一個程序. 程序和線程, 可以看作一個 1 : n 的關系.

OS 的排程, 有多個程序, 每個程序, 有多個線程 . 這樣了解這種 "數量" 關系應該還可以.

單線程

從代碼的視角, 即一段代碼按順序執行, 就是個一個單程序.

import time


# 我每天下班之後首先是: 煮飯, 做菜
def cook_rice():
    for i in range(3):
        print(f"cook rice ...{i}")
        time.sleep(1)


def cook_dishes():
    for i in range(4):
        print(f"cook dishes...{i}")
        time.sleep(0.5)


if __name__ == '__main__':
    cook_rice()
    cook_dishes()      
cook rice ...0
cook rice ...1
cook rice ...2
cook dishes...0
cook dishes...1
cook dishes...2
cook dishes...3      

這結果跟咱想的沒毛病. 先做飯, 然後再做菜... but, 真實中是這樣的嘛? 真實是 我先插上飯, 然後打開電腦, 放點音樂啥的, 就着手去洗菜, 切菜這些事情了.

可見真是情況是, 對于做飯這個程式, 我其實是可以同時做的. 這也是前面提到的多任務呀, 從生活來看, 這是非常自然的事情.而我一直覺點是, 寫代碼的本質, 是對現實世界的抽象和模拟. 是以, 洗菜做飯這種每天都要面對的基本事情, 同樣在程式設計從, 自然是歸類為 Python 基礎了.

多線程

也是用Python内置的一 threading 子產品 的 Thread 類, 來示範一波哦, 做飯和做菜是可以一起弄的.

import time
import threading


# 我每天下班之後首先是: 煮飯, 做菜
def cook_rice():
    for i in range(3):
        print(f"cook rice ...{i}")
        time.sleep(1)


def cook_dishes():
    for i in range(4):
        print(f"cook dishes...{i}")
        time.sleep(0.5)


if __name__ == '__main__':

    # 假設就各創一個線程來弄
    t1 = threading.Thread(target=cook_rice)
    t2 = threading.Thread(target=cook_dishes)

    # 啟動多線程
    t1.start()
    t2.start()      
cook rice ...0
cook dishes...0
cook dishes...1
cook rice ...1
cook dishes...2
cook dishes...3
cook rice ...2      

可以看到程序是不斷在 "切換", 用程序的術語說, 多個程序的在進行 "任務排程" , 這裡線程, 就, 嗯, 多個線程一起運作吧. (其實并未真正多個線程同時運作, 隻是排程太快而已 cpu).

主線程 - 等待所有子線程

跟多程序是一樣的, 主線程, 會預設等待所有的子程序, 都結束後才, 程式才會真正結束.

然後在建立線程, 傳參也是一樣的, 兩種方式 * args 和 ** kwargs. 這些都沒啥, 大緻有印象就行, 了解這個過程和特性更為重要, 怎麼寫, 會百度就好.

import time
import threading


# 我每天下班之後首先是: 煮飯, 做菜
def cook_rice(n):
    for i in range(n):
        print(f"cook rice ...{i}")
        time.sleep(1)


def cook_dishes(n):
    for i in range(n):
        print(f"cook dishes...{i}")
        time.sleep(0.5)


if __name__ == '__main__':

    # 假設就各創一個線程來弄
    t1 = threading.Thread(target=cook_rice, args=(3,))
    t2 = threading.Thread(target=cook_dishes, kwargs={"n":4})

    # 啟動多線程
    t1.start()
    t2.start()

    print("main threading done ....")      
cook rice ...0
cook dishes...0
main threading done ....
cook dishes...1
cook rice ...1
cook dishes...2
cook dishes...3
cook rice ...2
      

守護主線程

也是跟程序那塊一樣的, 從代碼視角看, 即設定一個屬性, daemon = True 即可.

import time
import threading


def cook_rice(n):
    for i in range(n):
        print(f"cook rice ...{i}")
        time.sleep(1)


if __name__ == '__main__':

    # 将 daemon 參數 傳為 True 即設定為了守護主線程.
    t1 = threading.Thread(target=cook_rice, args=(4,), daemon=True)

    # 啟動多線程
    t1.start()
    time.sleep(2)
    print("main threading done ....")      
cook rice ...0
cook rice ...1
cook rice ...2
main threading done ....      

我在測試的時候, 這部分發現線程和程序有一個不一樣的點, 在多個線程中, 如果隻将其中一個線程給 daemon 後, 當主程序結束, 其餘的線程還是會繼續執行; 而在多個程序中, 如果将其中一個程序給 daemon 後, 整個任務也就跟着停掉了. 很奇怪, 尚未弄明白這一點,暫時, 我後續再測測看.

阻塞-等待子程序結束

其實這有些莫名其妙, 直接運作不設定 daemon 不就行了嗎, 主要是啥, 我單純想熟悉下 join() 這個 API 而已啦.

import time
import threading


# 我每天下班之後首先是: 煮飯, 做菜
def cook_rice(n):
    for i in range(n):
        print(f"cook rice ...{i}")
        time.sleep(1)


if __name__ == '__main__':
    # 假設就各創一個線程來弄
    t1 = threading.Thread(target=cook_rice, args=(4,), daemon=True)

    # 啟動多線程
    t1.start()
    time.sleep(2)

    # 阻塞等待, 子程序結束
    t1.join()
    print("main threading done ....")      
cook rice ...0
cook rice ...1
cook rice ...2
cook rice ...3
main threading done ....      

自定義線程

沒啥應用, 也是純練習下程式設計技巧而已. 工作也偶爾會遇到這樣的情況, 就某個庫的某個功能, 可能不能滿足我的業務需求, 但我又不能全部寫一遍, 或者去 改掉它的源代碼, 改源代碼這事情我幹過, 那時候還是小白沒經驗, 最後導緻, 隻有我一個人能用, 很尴尬去改源代碼....

正确的做法是: 建立一個自定義類, 去繼承要修改的類, 然後 重寫其某個方法 . 這是正确的. 這裡呢要自定義, 其實就繼承上面調用的這 threading.Tread 類, 然後 start() 的部分, 點進去看一下源碼就知道了, 其實去 start() 會調用 它上面的 run( ) 方法, 是以, 重寫 run( ) 方法即可.

import time
import threading


class MyTread(threading.Thread):
    def __init__(self, *args):
        super().__init__()
        self.args = args

    @staticmethod
    def cook_rice(n):
        for i in range(n):
            print(f"cook rice ...{i}")

    def cook_dishes(self):
        for i in range(self.args[0]):
            print(f"cook dishes with {i}")

    def run(self):
        self.cook_rice(self.args[0])
        self.cook_dishes()


if __name__ == '__main__':
    t1 = MyTread(3)
    t1.start()

    t1.join()  # 等待子程序
    print("done")      

上篇就這吧, 也是對主線程的一個基本認識即可, 當然前提是對前面的 程序也有所了解, 這樣對比起來學習是非常快的. 這些代碼模闆, 守護主線程, 阻塞, 繼承重寫.. 都跟程序是一樣的哦,

而下篇呢, 就會聊點跟程序不一樣的地方,如線程中 資源能共享, 已經資源競争, 互斥鎖這些概念,, 還是蠻有趣的哦.

繼續閱讀