Python多任務教程:進程、線程、協程

来源:https://www.cnblogs.com/djdjdj123/archive/2023/07/08/17537285.html
-Advertisement-
Play Games

### 1.進程 進程是一個具有一定獨立功能的程式在一個數據集上的一次動態執行的過程,是操作系統進行資源分配和調度的一個獨立單位,是應用程式運行的載體。進程是一種抽象的概念,從來沒有統一的標准定義。進程一般由程式、數據集合和進程式控制制塊三部分組成。程式用於描述進程要完成的功能,是控制進程執行的指令集; ...


1.進程

進程是一個具有一定獨立功能的程式在一個數據集上的一次動態執行的過程,是操作系統進行資源分配和調度的一個獨立單位,是應用程式運行的載體。進程是一種抽象的概念,從來沒有統一的標准定義。進程一般由程式、數據集合和進程式控制制塊三部分組成。程式用於描述進程要完成的功能,是控制進程執行的指令集;數據集合是程式在執行時所需要的數據和工作區;程式控制塊包含進程的描述信息和控制信息是進程存在的唯一標誌。

進程具有的特征:

  • 動態性:進程是程式的一次執行過程,是臨時的,有生命期的,是動態產生,動態消亡的。
  • 併發性:任何進程都可以同其他進程一起併發執行。
  • 獨立性:進程是系統進行資源分配和調度的一個獨立單位。
  • 結構性:進程由程式、數據和進程式控制制塊三部分組成。

實現多進程

import multiprocessing
import time


def run1(sleep_time):
    while True:
        print("-- 1 --")
        time.sleep(sleep_time)


def run2(sleep_time):
    while True:
        print("-- 2 --")
        time.sleep(sleep_time)


def main():
    # 創建進程對象。
    # target:指定線程調用的函數名。註:等號後跟方法名不能加括弧,如果加了也能執行函數但threading功能無效
    # args:指定調用函數時傳遞的參數。註:args是一個數組變數參數,只傳一個參數時,需要在參數後面添加逗號
    p1 = multiprocessing.Process(target=run1, args=(1,))
    p2 = multiprocessing.Process(target=run2, args=(1,))

    # 啟用子進程
    p1.start()
    p2.start()

    # join方法等待子進程執行結束
    p1.join()
    p2.join()
    print("子進程結束")


if __name__ == "__main__":
    main()

運行上面代碼,查看任務管理器python的啟動進程數。
在這裡插入圖片描述
代碼中只啟動了兩個子進程,但是為什麼有3個python進程?這是因為,python會創建一個主進程(第1個進程),當運行到p1.start()時會創建一個子進程(第2個進程),當運行到p2.start()時又會創建一個子進程(第3個進程)

2.進程池

進程的創建和刪除是需要消耗電腦資源的,如果有大量任務需要多進程完成,則可能需要頻繁的創建刪除進程,這會給電腦帶來較多的資源消耗。進程池的出現解決了這個問題,它的原理是創建適當的進程放入進程池,等待待處理的事件,當處理完事件後進程不會銷毀,仍然在進程池中等待處理其他事件,直到事件全部處理完畢,進程退出。 進程的復用降低了資源的消耗。

實現進程池

import time, os

from multiprocessing import Pool


def worker(msg):
    start_time = time.time()
    print(F"{msg}開始執行,進程pid為{os.getpid()}")
    time.sleep(1)
    end_time = time.time()
    print(F"{msg}執行完畢,耗時{end_time - start_time}")


def main():
    po = Pool(3)    # 定義進程池最大進程數為3
    for i in range(10):
        # 每次迴圈會用空閑出的子進程調用目標
        po.apply_async(worker, args=(i,))   # 若調用的函數報錯,進程池中不會列印報錯信息

    po.close()  # 關閉進程池,關閉後,不再接收新的目標
    po.join()   # 等待進程池中所有子進程執行完,必須放在close()之後。若沒有join()操作,主進程執行完後直接關閉
    print("--end--")


if __name__ == "__main__":
    main()

3.線程

在早期的操作系統中並沒有線程的概念,進程是擁有資源和獨立運行的最小單位,也是程式執行的最小單位。任務調度採用的是時間片輪轉的搶占式調度方式,而進程是任務調度的最小單位,每個進程有各自獨立的一塊記憶體,使得各個進程之間記憶體地址相互隔離。後來,隨著電腦的發展,對CPU的要求越來越高,進程之間的切換開銷較大,已經無法滿足越來越複雜的程式的要求了。於是就發明瞭線程,線程是程式執行中一個單一的順序控制流程,是程式執行流的最小單元,是處理器調度和分派的基本單位。一個進程可以有一個或多個線程,各個線程之間共用程式的記憶體空間(也就是所在進程的記憶體空間)。一個標準的線程由線程ID,當前指令指針PC,寄存器和堆棧組成。而進程由記憶體空間(代碼,數據,進程空間,打開的文件)和一個或多個線程組成。

實現多線程

import time
import threading


def say(sleep_time):
    for i in range(5):
        print(f"說{i+1}下")
        time.sleep(sleep_time)

def dance():
    for i in range(10):
        print(f"跳{i+1}下")
        time.sleep(1)

def main():
    # 創建線程對象
    # target:指定線程調用的函數名。註:等號後跟方法名不能加括弧,如果加了也能執行函數但threading功能無效
    # args:指定調用函數時傳遞的參數。註:args是一個數組變數參數,只傳一個參數時,需要在參數後面添加逗號
    t1 = threading.Thread(target=say, args=(1,))
    t2 = threading.Thread(target=dance)

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

    # 查看正在運行的線程
    while True:
        now_threading = threading.enumerate()
        print(now_threading)
        # 當子線程全部運行結束後,僅剩1個主線程
        if len(now_threading) <= 1:
            break
        time.sleep(1)


if __name__ == "__main__":
    main()

多線程的資源競爭問題

因為多線程共用全局變數,當線程還沒執行完當前任務,操作系統就自動輪流調度執行其他任務,就可能會產生資源競爭的問題。

比如下例中,執行 g_num+=1 時,會將其分成3步執行:1.取值;2.運算;3.保存運算結果,在CPU執行任務時,若剛運行1 2 步就交替執行下一個任務,再返回來保存結果,因為共用全局變數,此時運算結果可能已被重新賦值。

import time
import threading

g_num = 0


def sum1(num):
    global g_num
    for i in range(num):
        g_num += 1
    print(F"sum1:{g_num}")


def sum2(num):
    global g_num
    for i in range(num):
        g_num += 1
    print(F"sum2:{g_num}")


def main():
    t1 = threading.Thread(target=sum1, args=(1000000,))
    t2 = threading.Thread(target=sum2, args=(1000000,))
    t1.start()
    t2.start()
    time.sleep(2)
    print(g_num)    # 執行後,預期結果為2000000;實際不是


if __name__ == "__main__":
    main()

執行結果
在這裡插入圖片描述
從結果可以看出,sum1和sum2不為1000000,總和不為2000000,這就是上面說的資源競爭問題

互斥鎖解決資源競爭問題

import threading
import time

# 定義一個全局變數
g_num = 0

# 創建一個互斥鎖,預設是沒有上鎖的
mutex = threading.Lock()


def sum1(num):
    global g_num
    # mutex.acquire()     # 若在此處上鎖,要等下麵迴圈執行完才會解鎖,若迴圈時間太長,會導致另外的線程堵塞等待。
    for i in range(num):
        # 上鎖,如果之前沒有被上鎖,那麼此時上鎖成功。 上鎖原則:一般對產生資源競爭的代碼上鎖。如果上鎖之前 已經被上鎖了,那麼此時會堵塞在這裡,直到 這個鎖被解開為止。
        mutex.acquire()
        g_num += 1
        # 解鎖
        mutex.release()
    print("-----in test1 g_num=%d----" % g_num)


def sum2(num):
    global g_num
    for i in range(num):
        mutex.acquire()
        g_num += 1
        mutex.release()
    print("-----in test2 g_num=%d=----" % g_num)


def main():
    t1 = threading.Thread(target=sum1, args=(1000000,))
    t2 = threading.Thread(target=sum2, args=(1000000,))

    t1.start()
    t2.start()

    # 等待上面的2個線程執行完畢....
    time.sleep(2)

    print("-----in main Thread g_num = %d---" % g_num)


if __name__ == "__main__":
    main()

運行結果
在這裡插入圖片描述

死鎖

線上程間共用多個資源的時候,如果兩個線程分別占用部分資源並且同時等待對方的資源,就會造成死鎖。儘管死鎖很少發生,但一旦發生就會造成應用停止響應。下麵看一個死鎖例子。

import time
import threading


# 創建多個鎖
mutexA = threading.Lock()
mutexB = threading.Lock()


def print1():
    mutexA.acquire()
    time.sleep(2)   # 等待B鎖穩定
    print("列印A1")
    mutexB.acquire()
    print("列印B1")
    mutexB.release()
    mutexA.release()


def print2():
    mutexB.acquire()
    time.sleep(1)   # 等待A鎖穩定
    print("列印B2")
    mutexA.acquire()
    print("列印A2")
    mutexA.release()
    mutexB.release()


def main():
    t1 = threading.Thread(target=print1)
    t2 = threading.Thread(target=print2)

    t1.start()
    t2.start()


if __name__ == "__main__":
    main()

執行結果
在這裡插入圖片描述
避免死索辦法:1、添加超時時間;2、銀行家演算法(讓鎖按預期上鎖和解鎖)

4.協程

協程,又稱微線程。協程的作用是在執行函數A時可以隨時中斷去執行函數B,然後中斷函數B繼續執行函數A(可以自由切換)。但這一過程並不是函數調用,這一整個過程看似像多線程,然而協程只有一個線程執行。

協程的優勢:

  • 執行效率極高,因為子程式切換(函數)不是線程切換,由程式自身控制,沒有切換線程的開銷。所以與多線程相比,線程的數量越多,協程性能的優勢越明顯。
  • 不需要多線程的鎖機制,因為只有一個線程,也不存在同時寫變數衝突,在控制共用資源時也不需要加鎖,因此執行效率高很多。

gevent

gevent是第三方庫,通過 greenlet 實現 coroutine,創建、調度的開銷比 線程(thread) 還小,因此程式內部的 執行流 效率高。

其基本思想是:當一個greenlet遇到IO操作時,比如訪問網路,就自動切換到其他的greenlet,等到IO操作完成,再在適當的時候切換回來繼續執行。由於IO操作非常耗時,經常使程式處於等待狀態,有了gevent為我們自動切換協程,就保證總有greenlet在運行,而不是等待IO。

gevent常用方法:

  • gevent.spawn() 創建一個普通的Greenlet對象並切換
  • gevent.spawn_later(seconds=3) 延時創建一個普通的Greenlet對象並切換
  • gevent.spawn_raw() 創建的協程對象屬於一個組
  • gevent.getcurrent() 返回當前正在執行的greenlet
  • gevent.joinall(jobs) 將協程任務添加到事件迴圈,接收一個任務列表
  • gevent.wait() 可以替代join函數等待迴圈結束,也可以傳入協程對象列表
  • gevent.kill() 殺死一個協程
  • gevent.killall() 殺死一個協程列表裡的所有協程
  • monkey.patch_all() 非常重要,會自動將python的一些標準模塊替換成gevent框架
import gevent


def task(n):
    for i in range(n):
        print(gevent.getcurrent(), i)


if __name__ == '__main__':
    g1 = gevent.spawn(task, 3)
    g2 = gevent.spawn(task, 3)
    g3 = gevent.spawn(task, 3)

    g1.join()
    g2.join()
    g3.join()

運行結果
在這裡插入圖片描述
可以看到3個greenlet是依次運行而不是交替運行。要讓greenlet交替運行,可以通過gevent.sleep()交出控制權:

import gevent


def task(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        gevent.sleep(1)


if __name__ == '__main__':
    g1 = gevent.spawn(task, 3)
    g2 = gevent.spawn(task, 3)
    g3 = gevent.spawn(task, 3)

    g1.join()
    g2.join()
    g3.join()

運行結果
在這裡插入圖片描述
當然在實際的代碼里,我們不會用gevent.sleep()去切換協程,而是在執行到IO操作時gevent會自動完成,所以gevent需要將Python自帶的一些標準庫的運行方式由阻塞式調用變為協作式運行。這一過程在啟動時通過monkey patch完成:

import time
import gevent
from gevent import monkey
# 猴子補丁,會自動將python的一些標準模塊替換成gevent框架。慎用,它創造了“隱式的副作用”,如果出現問題 它很多時候是極難調試的。
monkey.patch_all()  # 註意:若導出的模塊函數不會被替換,比如from time import sleep,sleep不會被替換


def task(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        time.sleep(1)   # 會被gevent自動替換為gevent.sleep()


if __name__ == '__main__':
    g1 = gevent.spawn(task, 3)
    g2 = gevent.spawn(task, 3)
    g3 = gevent.spawn(task, 3)

    g1.join()
    g2.join()
    g3.join()

執行結果
在這裡插入圖片描述
上面的流程看起來比較繁瑣,可以使用 gevent.joinall() 方法簡化流程:

import time
import gevent
from gevent import monkey
# 猴子補丁,會自動將python的一些標準模塊替換成gevent框架。慎用,它創造了“隱式的副作用”,如果出現問題 它很多時候是極難調試的。
monkey.patch_all()  # 註意:若導出的模塊函數不會被替換,比如from time import sleep,sleep不會被替換
'''
學習中遇到問題沒人解答?小編創建了一個Python學習交流群:711312441
尋找有志同道合的小伙伴,互幫互助,群里還有不錯的視頻學習教程和PDF電子書!
'''

def task(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        time.sleep(1)   # 會被gevent自動替換為gevent.sleep()


if __name__ == '__main__':
    gevent.joinall([
        gevent.spawn(task, 4),
        gevent.spawn(task, 4),
        gevent.spawn(task, 4),
    ])

執行結果
在這裡插入圖片描述


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ## 1.1 概述 > 說白了就是鍵值對的映射關係 > > 不會丟失數據本身關聯的結構,但不關註數據的順序 > > 是一種可變類型 ```py 格式:dic = {鍵:值, 鍵:值} ``` * 鍵的類型:字典的鍵可以是任何不可變的類型,如浮點數,字元串,元組 ## 1.2 函數dict 可以從其他 ...
  • ### 目錄 *1:什麼是AQS?* *2:AQS都有那些用途?* *3:我們如何使用AQS* *4:AQS的實現原理* *5:對AQS的設計與實現的一些思考* ### 1:什麼是AQS ​ 隨著電腦的算力越來越強大,各種各樣的並行編程模型也隨即踴躍而來,但當我們要在並行計算中使用共用資源的時候, ...
  • 電腦COM口數據測試一、基本使用流程 程式需要以管理員身份運行,COM口迴路測試需短接2,3pin,測試時候使用控制台,配置測試相關路徑,併在測試完成後 1.測試配置路徑D:\bigdata\INI\FWCOM.ini 2.測試完成後需要在路徑D:\bigdata\LOG\生成測試FWCOM.lo ...
  • # C++ 慣用法之 Copy-Swap 拷貝交換 > 這是“C++ 慣用法”合集的第 3 篇,前面 2 篇分別介紹了 RAII 和 PIMPL 兩種慣用法: > > - [RAII: Resouce Acquistion Is Initialization](https://www.cnblogs ...
  • **本文首先介紹了Django模板系統的基礎知識,接著探討瞭如何安裝和配置Django模板系統,然後深入解析了Django模板的基本結構、標簽和過濾器的用法,闡述瞭如何在模板中展示模型數據,最後使用一個實際項目的例子來演示如何在實際開發中使用Django模板系統。** ## Django模板系統的簡 ...
  • # 1.用戶角色配置 ![image.png](https://cdn.nlark.com/yuque/0/2023/png/38371876/1688636206975-acd927ca-1559-4236-85f2-07283999d50b.png#averageHue=%23f3f2f2&cl ...
  • Java存在很多ORM框架,MyBaits框架是我們項目中使用得最多也是最願意推薦的框架,它既有數據表和Java對象映射功能,又有原生SQL的特性。在與SpringBoot集成上,和其他框架一樣,可以做到全註解化,無XML配置…… ...
  • 要用Python寫一個網站,你可以使用Python的Web框架來開發。常見的Python Web框架包括Django、Flask、Bottle等。以下是一個簡單的使用Flask框架開發的示例。 ### 1. 安裝Flask 在開始開發之前,你需要安裝Flask框架。你可以使用以下命令來安裝: ``` ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...