Python 中執行緒和程式

Kun發表於2022-03-01

執行緒和程式

一、 什麼是程式 / 執行緒

1、 引論

眾所周知,CPU是計算機的核心,它承擔了所有的計算任務。而作業系統是計算機的管理者,是一個大管家,它負責任務的排程,資源的分配和管理,統領整個計算機硬體。應用程式是具有某種功能的程式,程式執行與作業系統之上

2、 執行緒

在很早的時候計算機並沒有執行緒這個概念,但是隨著時代的發展,只用程式來處理程式出現很多的不足。如當一個程式堵塞時,整個程式會停止在堵塞處,並且如果頻繁的切換程式,會浪費系統資源。所以執行緒出現了

執行緒是能擁有資源和獨立執行的最小單位,也是程式執行的最小單位。一個程式可以擁有多個執行緒,而且屬於同一個程式的多個執行緒間會共享該進行的資源

3、 程式

程式時一個具有一定功能的程式在一個資料集上的一次動態執行過程。程式由程式,資料集合和程式控制塊三部分組成。程式用於描述程式要完成的功能,是控制程式執行的指令集;資料集合是程式在執行時需要的資料和工作區;程式控制塊(PCB)包含程式的描述資訊和控制資訊,是程式存在的唯一標誌

4、 區別

  1. 一個程式由一個或者多個執行緒組成,執行緒是一個程式中程式碼的不同執行路線
  2. 切換程式需要的資源比切換執行緒的要多的多
  3. 程式之間相互獨立,而同一個程式下的執行緒共享程式的記憶體空間(如程式碼段,資料集,堆疊等)。某程式內的執行緒在其他程式不可見。換言之,執行緒共享同一片記憶體空間,而程式各有獨立的記憶體空間

5、 使用

在Python中,通過兩個標準庫 threadThreading提供對執行緒的支援,threadingthread進行了封裝。threading模組中提供了 Thread,Lock, RLOCK, Condition等元件

二、 多執行緒使用

在Python中執行緒和程式的使用就是通過Thread這個類。這個類在我們的threadthreading模組中。我們一般通過threading匯入

預設情況下,只要在直譯器中,如果沒有報錯,則說明執行緒可用

from threading import Thread

1、 常用方法

Thread.run(self)  # 執行緒啟動時執行的方法,由該方法呼叫 target 引數所指定的函式
Thread.start(self)  # 啟動執行緒,start 方法就是去呼叫 run 方法
Thread.terminate(self)  # 強制終止執行緒
Thread.join(self, timeout)  # 阻塞呼叫,主執行緒進行等待
Thread.setDaemon(self, daemonic)  # 將子執行緒設定為守護執行緒
Thread.getName(self, name)  # 獲取執行緒名稱
Thread.setName(self, name)  # 設定執行緒名稱

2、 常用引數

引數 說明
target 表示呼叫物件,即子執行緒要執行的任務
name 子執行緒的名稱
args 傳入 target 函式中的位置引數,是一個元組,引數後必須新增逗號

3、 多執行緒的應用

3.1 重寫執行緒法

import time, queue, threading


class MyThread(threading.Thread):

    def __init__(self):
        super().__init__()
        self.daemon = True  # 開啟守護模式
        self.queue = queue.Queue(3)  # 開啟佇列物件,儲存三個任務
        self.start()  # 例項化的時候直接啟動執行緒,不需要手動啟動執行緒

    def run(self) -> None:  # run方法執行緒自帶的方法,內建方法,線上程執行時會自動呼叫
        while True:  # 不斷處理任務
            func, args, kwargs = self.queue.get()
            func(*args, **kwargs)  # 呼叫函式執行任務 元組不定長記得一定要拆包
            self.queue.task_done()  # 解決一個任務就讓計數器減一,避免阻塞

    # 生產者模型
    def submit_tasks(self, func, args=(), kwargs={}):  # func為要執行的任務,加入不定長引數使用(預設使用預設引數)
        self.queue.put((func, args, kwargs))  # 提交任務

    # 重寫join方法
    def join(self) -> None:
        self.queue.join()  # 檢視佇列計時器是否為0 任務為空 為空關閉佇列
        
        
def f2(*args, **kwargs):
    time.sleep(2)
    print("任務2完成", args, kwargs)

# 例項化執行緒物件
mt = MyThread()
# 提交任務
mt.submit_tasks(f2, args=("aa", "aasd"), kwargs={"a": 2, "s": 3})

# 讓主執行緒等待子執行緒結束再結束
mt.join()

守護模式:

  • 主執行緒在其他非守護執行緒執行完畢後才算執行完畢(守護執行緒在此時就被回收)。因為主執行緒的結束意味著程式的結束,程式整體的資源都將被回收,而程式必須保證非守護執行緒都執行完畢後才能結束

3.2 直接呼叫法

def f2(i):
    time.sleep(2)
    print("任務2完成", i)

lis = []
for i in range(5):
    t = Thread(target=f2, args=(i,))
    t.start()  # 啟動 5 個執行緒
    lis.append(t)

for i in lis:
    i.join()  # 執行緒等待

4、 執行緒間資料的共享

現在我們程式程式碼中,有多個執行緒, 並且在這個幾個執行緒中都會去 操作同一部分內容,那麼如何實現這些資料的共享呢?

這時,可以使用 threading庫裡面的鎖物件 Lock 去保護

Lock 物件的acquire方法 是申請鎖

每個執行緒在操作共享資料物件之前,都應該申請獲取操作權,也就是呼叫該共享資料物件對應的鎖物件的acquire方法,如果執行緒A 執行了 acquire()方法,別的執行緒B 已經申請到了這個鎖, 並且還沒有釋放,那麼 執行緒A的程式碼就在此處 等待 執行緒B 釋放鎖,不去執行後面的程式碼。

直到執行緒B 執行了鎖的 release 方法釋放了這個鎖, 執行緒A 才可以獲取這個鎖,就可以執行下面的程式碼了

如:

import threading

var = 1
# 新增互斥鎖,並且拿到鎖
lock = threading.Lock()

# 定義兩個執行緒要用做的任務
def func1():
    global var  # 宣告全域性變數
    for i in range(1000000):
        lock.acquire()  # 操作前上鎖
        var += i
        lock.release()  # 操作完後釋放鎖
        
def func2():
    global var  # 宣告全域性變數
    for i in range(1000000):       
		lock.acquire()  # 操作前上鎖    
        var -= i
        lock.release()  # 操作完後釋放鎖
        
# 建立2個執行緒
t1 = threading.Thread(target=func1)
t2 = threading.Thread(target=func2)
t1.start()
t2.start()
t1.join()
t2.join()
print(var)

到在使用多執行緒時,如果資料出現和自己預期不符的問題,就可以考慮是否是共享的資料被呼叫覆蓋的問題

使用 threading庫裡面的鎖物件 Lock去保護

三、 多程式使用

1、 簡介

Python中的多程式是通過multiprocessing包來實現的,和多執行緒的threading.Thread差不多,它可以利用multiprocessing.Process物件來建立一個程式物件。這個程式物件的方法和執行緒物件的方法差不多也有start(), run(), join()等方法,其中有一個方法不同Thread執行緒物件中的守護執行緒方法是setDeamon,而Process程式物件的守護程式是通過設定daemon屬性來完成的

2、 應用

2.1 重寫程式法

import time
from multiprocessing import Process


class MyProcess(Process):  # 繼承Process類
    def __init__(self, target, args=(), kwargs={}):
        super(MyProcess, self).__init__()
        self.daemon = True  # 開啟守護程式
        self.target = target
        self.args = args
        self.kwargs = kwargs
        self.start()  # 自動開啟程式

    def run(self):
        self.target(*self.args, **self.kwargs)


def fun(*args, **kwargs):
    print(time.time())
    print(args[0])


if __name__ == '__main__':
    lis = []
    for i in range(5):
        p = MyProcess(fun, args=(1, ))
        lis.append(p)
    for i in lis:
        i.join()  # 讓程式等待

守護模式:

  • 主程式在其程式碼結束後就已經算執行完畢了(守護程式在此時就被回收),然後主程式會一直等非守護的子程式都執行完畢後回收子程式的資源(否則會產生殭屍程式),才會結束

2.2 直接呼叫法

import time
from multiprocessing import  Process

def fun(*args, **kwargs):
    print(time.time())
    print(args[0])

if __name__ == '__main__':
    lis = []
    for i in range(5):
        p = Process(target=fun, args=(1, ))
        lis.append(p)
    for i in lis:
        i.join()  # 讓程式等待

3、 程式之間的資料共享

3.1 Lock 方法

其使用方法和執行緒的那個 Lock 使用方法類似

3.2 Manager 方法

Manager的作用是提供多程式共享的全域性變數,Manager()方法會返回一個物件,該物件控制著一個服務程式,該程式中儲存的物件執行其他程式使用代理進行操作

Manager支援的型別有:list,dict,Namespace,Lock,RLock,Semaphore,BoundedSemaphore,Condition,Event,Queue,Value和Array

語法:

from multiprocessing import Process, Lock, Manager

def f(n, d, l, lock):
    lock.acquire()
    d[str(n)] = n
    l[n] = -99
    lock.release()

if __name__ == '__main__':
    lock = Lock()
    with Manager() as manager:
        d = manager.dict()  # 空字典
        l = manager.list(range(10))  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        # 啟動10個程式,不同的程式對d和l中的不同元素進行操作
        for i in range(10):
            p = Process(target=f, args=(i, d, l, lock))
            p.start()
            p.join()

        print(d)
        print(l)

四、 池併發

1、 語法

執行緒池的基類是 concurrent.futures 模組中的 ExecutorExecutor 提供了兩個子類,即 ThreadPoolExecutorProcessPoolExecutor,其中 ThreadPoolExecutor 用於建立執行緒池,而 ProcessPoolExecutor 用於建立程式池

如果使用執行緒池/程式池來管理併發程式設計,那麼只要將相應的 task 函式提交給執行緒池/程式池,剩下的事情就由執行緒池/程式池來搞定

Exectuor 提供瞭如下常用方法:

  • submit(fn, *args, *kwargs):將 fn 函式提交給執行緒池。args 代表傳給 fn 函式的引數,*kwargs 代表以關鍵字引數的形式為 fn 函式傳入引數
  • map(func, *iterables, timeout=None, chunksize=1):該函式類似於全域性函式 map(func, *iterables),只是該函式將會啟動多個執行緒,以非同步方式立即對 iterables 執行 map 處理。
  • shutdown(wait=True):關閉執行緒池

程式將 task 函式提交(submit)給執行緒池後,submit 方法會返回一個 Future 物件,Future 類主要用於獲取執行緒任務函式的返回值。由於執行緒任務會在新執行緒中以非同步方式執行,因此,執行緒執行的函式相當於一個“將來完成”的任務,所以 Python 使用 Future 來代表

Future 提供瞭如下方法:

  • cancel():取消該 Future 代表的執行緒任務。如果該任務正在執行,不可取消,則該方法返回 False;否則,程式會取消該任務,並返回 True。
  • cancelled():返回 Future 代表的執行緒任務是否被成功取消。
  • running():如果該 Future 代表的執行緒任務正在執行、不可被取消,該方法返回 True。
  • done():如果該 Funture 代表的執行緒任務被成功取消或執行完成,則該方法返回 True。
  • result(timeout=None):獲取該 Future 代表的執行緒任務最後返回的結果。如果 Future 代表的執行緒任務還未完成,該方法將會阻塞當前執行緒,其中 timeout 引數指定最多阻塞多少秒。
  • exception(timeout=None):獲取該 Future 代表的執行緒任務所引發的異常。如果該任務成功完成,沒有異常,則該方法返回 None。
  • add_done_callback(fn):為該 Future 代表的執行緒任務註冊一個“回撥函式”,當該任務成功完成時,程式會自動觸發該 fn 函式

2、 獲取 CPU 數量

from multiprocessing import cpu_count  # cpu核心數模組,其可以獲取 CPU 核心數

n = cpu_count()  # 獲取cpu核心數

3、 執行緒池

使用執行緒池來執行執行緒任務的步驟如下:

  1. 呼叫 ThreadPoolExecutor 類的構造器建立一個執行緒池
  2. 定義一個普通函式作為執行緒任務
  3. 呼叫 ThreadPoolExecutor 物件的 submit() 方法來提交執行緒任務
  4. 當不想提交任何任務時,呼叫 ThreadPoolExecutor 物件的 shutdown() 方法來關閉執行緒池
from concurrent.futures import ThreadPoolExecutor
import threading
import time
# 定義一個準備作為執行緒任務的函式
def action(max):
    my_sum = 0
    for i in range(max):
        print(threading.current_thread().name + '  ' + str(i))
        my_sum += i
    return my_sum

# 建立一個包含2條執行緒的執行緒池
pool = ThreadPoolExecutor(max_workers=2)
# 向執行緒池提交一個task, 50會作為action()函式的引數
future1 = pool.submit(action, 50)
# 向執行緒池再提交一個task, 100會作為action()函式的引數
future2 = pool.submit(action, 100)
def get_result(future):
	print(future.result())
    
# 為future1新增執行緒完成的回撥函式
future1.add_done_callback(get_result)
# 為future2新增執行緒完成的回撥函式
future2.add_done_callback(get_result)
# 判斷future1代表的任務是否結束
print(future1.done())
time.sleep(3)
# 判斷future2代表的任務是否結束
print(future2.done())
# 檢視future1代表的任務返回的結果
print(future1.result())
# 檢視future2代表的任務返回的結果
print(future2.result())
# 關閉執行緒池
pool.shutdown()  # 序可以使用 with 語句來管理執行緒池,這樣即可避免手動關閉執行緒池

最佳執行緒數目 = ((執行緒等待時間+執行緒CPU時間)/執行緒CPU時間 )* CPU數目

也可以低於 CPU 核心數

3、 程式池

使用執行緒池來執行執行緒任務的步驟如下:

  1. 呼叫 ProcessPoolExecutor 類的構造器建立一個執行緒池
  2. 定義一個普通函式作為程式程任務
  3. 呼叫 ProcessPoolExecutor 物件的 submit() 方法來提交執行緒任務
  4. 當不想提交任何任務時,呼叫 ProcessPoolExecutor 物件的 shutdown() 方法來關閉執行緒池

關於程式的開啟程式碼一定要放在if __name__ == '__main__':程式碼之下,不能放到函式中或其他地方

開啟程式的技巧

from concurrent.futures import ProcessPoolExecutor

pool = ProcessPoolExecutor(max_workers=cpu_count())  # 根據cpu核心數開啟多少個程式

開啟程式的數量最好低於最大 CPU 核心數

相關文章