首頁 > 軟體

深入瞭解Python 中執行緒和程序區別

2022-03-02 10:01:01

一、 什麼是程序 / 執行緒

1、 引論

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

2、 執行緒

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

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

3、 程序

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

4、 區別

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

5、 使用

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

二、 多執行緒使用

在Python中執行緒和程序的使用就是通過 Thread 這個類。這個類在我們的 thread 和 threading 模組中。我們一般通過 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 模組中的 Executor , Executor 提供了兩個子類,即 ThreadPoolExecutor 和 ProcessPoolExecutor ,其中 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、 執行緒池

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

  • 呼叫 ThreadPoolExecutor 類的構造器建立一個執行緒池
  • 定義一個普通函數作為執行緒任務
  • 呼叫 ThreadPoolExecutor 物件的 submit() 方法來提交執行緒任務
  • 當不想提交任何任務時,呼叫 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、 程序池

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

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

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

開啟程序的技巧:

from concurrent.futures import ProcessPoolExecutor

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

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

到此這篇關於深入瞭解Python 中執行緒和程序區別的文章就介紹到這了,更多相關Python 中執行緒和程序內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com