Python的多工程式設計

wuyibnsk發表於2021-10-25

文章轉載自東凌閣

GoodMai 好買網

--------------------------------------------------------------------------------------------------------------

前言

Python程式程式碼都是按自上而下的順序載入並執行的,但實際需要程式碼處理的任務並不都是需要按部就班的順序執行,通常為提高程式碼執行的效率,需要多個程式碼執行任務同時執行,也就是多工程式設計的需求。

基本的計算機模型是由CPU、RAM及各種資源(鍵盤、硬碟、顯示卡、網路卡等)組成,程式碼的執行過程,實際就是CPU和相關暫存器及RAM之間的相關處理的過程。在單核CPU場景下,一段程式碼交由CPU執行前,都會處於就緒佇列中,CPU執行時很快就會返回該段程式碼的結果,所以不同程式的程式碼是輪流由CPU執行的,由於CPU執行速度很快,在表現上仍會被感覺是同時執行的。不同就緒佇列間的讀入與結果儲存被稱之為上下文切換,由於程式間切換會產生一定的時間等待及資源的消耗,所以為了減少等待時間和資源的消耗,就引入了執行緒的設計。執行緒是當程式的佇列被授權佔用CPU時,該程式的所有執行緒佇列在共享該程式資源的環境下按優先順序由CPU執行。無論是程式還是執行緒,其佇列及資源切換都是由作業系統進行控制的,同時執行緒的切換也是非常消耗效能的,為了使各執行緒的排程更節約資源,就出現了協程的設計。協程是在程式或執行緒環境下執行的,其擁有自己的暫存器上下文和棧,排程是完全由使用者控制的,相當於函式方法的排程。對於多工程式設計,若要實現程式碼的多工高效率執行,我們要明晰如下這幾個概念的特點及其區別,才能根據實際需求,選用最佳的多工程式設計方法。

 

並行

指在同一時刻有多個程式的指令在多個處理器上同時執行。

併發

是指在同一時刻只能有一個程式的指令執行,但多個程式指令被快速輪換執行,使得在宏觀上具有多個程式同時執行的效果。

程式

程式是程式的執行態,程式間資料共享需要藉助外部儲存空間。

執行緒

執行緒是程式的組成部分,一個程式可以包含一個或多個執行緒,同一程式內執行緒間資料共享屬於內部共享。

協程

協程是一種使用者態的輕量級執行緒,一個程式可以包含一個或多個協程,也可以在一個執行緒包含一個或多個協程。協程的排程完全由使用者控制,同一程式內協程間資料共享屬於內部共享。

多執行緒處理

由於Python是動態編譯的語言,與C/C++、Java等靜態語言不同,它是在執行時一句一句程式碼地邊編譯邊執行的。用C語言實現的Python直譯器,通常稱為CPython,也是Python環境預設的編譯器。在Cpython直譯器中,為防止多個執行緒同時執行同一 Python 的程式碼段,確保執行緒資料安全,引入了全域性直譯器鎖(GIL, Global Interpreter Lock)的處理機制, 該機制相當於一個互斥鎖,所以即便一個程式下開啟了多執行緒,但同一時刻只能有一個執行緒被執行。所以Python 的多執行緒是偽執行緒,效能並不高,也無法利用CPU多核的優勢。

 

另,GIL並不是Python的特性,他是在實現Python直譯器(Cpython)時所引入的一個概念,GIL保護的是直譯器級的資料,保護使用者自己的資料仍需要自己加鎖處理。在預設情況下,由於GIL的存在,為了使多執行緒(threading)執行效率更高,需要使用join方法對無序的執行緒進行阻塞,如下程式碼可以看到區別。

```

from multiprocessing import Process

import threading

import os,time

 

l=[]

stop=time.time()

def work():

     global stop

     time.sleep(2)

     print('===>',threading.current_thread().name)

     stop=time.time()

 

def test1():

     for i in range(400):

         p=threading.Thread(target=work,name="test"+str(i))

         l.append(p)

         p.start()

 

def test2():

     for i in range(400):

         p=threading.Thread(target=work,name="test"+str(i))

         l.append(p)

         p.start()

 

     for p in l:

         p.join()

 

if __name__ == '__main__':

     print("CPU Core:",os.cpu_count()) #本機為4核

     print("Worker: 400") #測試執行緒數

 

     start=time.time()

     test1()

     active_count=threading.active_count()

     while (active_count>1):

         active_count=threading.active_count()

         continue

     test1_result=stop-start

 

     start=time.time()

     l=[]

     test2()

     active_count=threading.active_count()

     while (active_count>1):

         active_count=threading.active_count()

         continue

    

     print('Thread run time is %s' %(test1_result))

print('Thread join run time is %s' %(stop-start))

```

 

 

執行結果如下:

```

Thread run time  is 4.829492807388306  

Thread join  run time  is 2.053645372390747

```

 

 

由上結果可以看到, 多執行緒時join阻塞後執行效率提高了很多。

多程式與多執行緒

多工程式設計的本質是CPU佔用方法的排程處理,對於python下多工處理有多種程式設計方法可供選擇,分別有多程式(multiprocessing)、多執行緒(threading)及非同步協程(Asyncio),在實際使用中該如何選擇呢?我們先看如下一段程式的執行效果。

```

from multiprocessing import Process

from threading import Thread

import os,time

 

l=[]

def work():

     res=0

     for i in range(100000000):

         res*=i

 

def test1():

     for i in range(4):

         p=Process(target=work)

         l.append(p)

         p.start()

 

def test2():

     for i in range(4):

         p=Thread(target=work)

         l.append(p)

         p.start()

 

     for p in l:

         p.join()

 

if __name__ == '__main__':

     print("CPU Core:",os.cpu_count()) #本機為4核

     print("Worker: 4") #工作執行緒或子程式數

 

     start=time.time()

     test1()

     while (l[len(l)-1].is_alive()):

         continue

     stop=time.time()

     print('Process run time is %s' %(stop-start))

    

     start=time.time()

     l=[]

     test2()

     while (l[len(l)-1].is_alive()):

         continue

     stop=time.time()

     print('Thread run time is %s' %(stop-start))

```

 

執行結果如下:

```

CPU Core: 4  

Worker: 4  

Process run time  is 11.030176877975464  

Thread run time  is 17.0117769241333

```

 

 

從上面的結果,我們可以看到同一個函式用Process及Thread 不同的方法,執行的時間是不同的,為什麼會產生這樣的差異?

多程式(multiprocessing)方法使用子程式而非執行緒,其有效地繞過了全域性直譯器鎖GIL(Global Interpreter Lock), 並充分利用了多核CPU的效能,所以在多核CPU環境下,其比多執行緒方式效率要高。

 

協程

又稱為微執行緒,協程也可被看作是被標註的函式,不同被表注函式的執行和切換就是協程的切換,其完全由程式設計者自行控制。協程一般是使用 gevent庫,在早期這個庫用起來比較麻煩,所以在python 3.7以後的版本,對協程的使用方法做了最佳化。執行程式碼如下:

```

import asyncio

import time

 

async def work(i):

     await asyncio.sleep(2)

     print('===>',i)

 

async def main():

     start=time.time()

     l=[]

     for i in range(400):

         p=asyncio.create_task(work(i))

         l.append(p)

 

     for p in l:

         await p

 

     stop=time.time()

     print('run time is %s' %(stop-start))

 

asyncio.run(main())

```

 

執行結果如下:

```

run time is   2.0228068828582764

```

 

 

 

另,預設環境下,協程是在單執行緒模式下執行的非同步操作,其並不能發揮多處理器的效能。為了提升執行效率,可以在多程式中執行協程呼叫方法,程式碼用例如下:

```

from multiprocessing import Process

import asyncio

import os,time

 

l=[]

async_result=0

async def work1():

    res=0

    for i in range(100000000):

        res*=i

 

# 協程入口

async def async_test():

    m=[]

    for i in range(4):

        p=asyncio.create_task(work1())

        m.append(p)

 

    for p in m:

        await p

 

async def async_test1():

    await asyncio.create_task(work1())

 

def async_run():

    asyncio.run(async_test1())

 

# 多程式入口

def test1():

    for i in range(4):

        p=Process(target=async_run)

        l.append(p)

        p.start()

 

if __name__ == '__main__':

    print("CPU Core:",os.cpu_count()) #本機為4核

    print("Worker: 4") #工作執行緒或子程式數

    start=time.time()

    asyncio.run(async_test())

    stop=time.time()

    

    print('Asyncio run time is %s' %(stop-start))

 

    start=time.time()

    test1()

    while (l[len(l)-1].is_alive()):

        continue

    stop=time.time()

 

    print('Process Asyncio run time is %s' %(stop-start))

```

 

 

執行結果如下:

```

CPU Core :   4  

Worker :   4  

Asyncio run time is   18.89663052558899  

Process Asyncio run time is   10.865562438964844

```

 

如上結果,在多程式中呼叫多協程的方法,執行效率明顯提高。

 

多工程式設計選擇

如上所結果是否是就決定一定要選擇多程式(multiprocessing)模式呢?我們再看下如下程式碼:

 

```

from multiprocessing import Process

from threading import Thread

import os,time

 

l=[]

# 僅計算

def work1():

     res=0

     for i in range(100000000):

         res*=i

 

# 僅輸出

def work2():

     time.sleep(2)

     print('===>')

 

# 多程式,僅計算

def test1():

     for i in range(4):

         p=Process(target=work1)

         l.append(p)

         p.start()

 

# 多程式,僅輸出

def test_1():

     for i in range(400):

         p=Process(target=work2)

         l.append(p)

         p.start()

 

# 多執行緒,僅計算

def test2():

     for i in range(4):

         p=Thread(target=work1)

         l.append(p)

         p.start()

 

     for p in l:

         p.join()

 

# 多執行緒,僅輸出

def test_2():

     for i in range(400):

         p=Thread(target=work2)

         l.append(p)

         p.start()

 

     for p in l:

         p.join()

 

if __name__ == '__main__':

     print("CPU Core:",os.cpu_count()) #本機為4核

 

     start=time.time()

     test1()

     while (l[len(l)-1].is_alive()):

         continue

     stop=time.time()

     test_result=stop-start

    

     start=time.time()

     l=[]

     test_1()

     while (l[len(l)-1].is_alive()):

         continue

     stop=time.time()

     test1_result=stop-start

 

     start=time.time()

     l=[]

     test2()

     while (l[len(l)-1].is_alive()):

         continue

     stop=time.time()

     test2_result=stop-start

     start=time.time()

     l=[]

     test_2()

     while (l[len(l)-1].is_alive()):

         continue

     stop=time.time()

     test3_result=stop-start

     print('Process run time is %s' %(test_result))

     print('Process I/O run time is %s' %(test1_result))

     print('Thread run time is %s' %(test2_result))

     print('Thread I/O run time is %s' %(stop-start))

```

執行結果如下:

```

Process run time  is 10.77662968635559  

Process I/O run time  is 2.9869778156280518  

Thread run time  is 16.842355012893677  

Thread I/O run time  is 2.024587869644165

```

 

由結果可看,在僅計算的操作時,多程式效率比較高,在僅輸出的操作時,多執行緒的效率比較高,所以在實際使用中要根據實際情況測試決定。通用的建議如下:

·  多執行緒(threading)用於IO密集型,如socket,爬蟲,web

·  多程式(multiprocessing)用於計算密集型,如資料分析

 

 




來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70008135/viewspace-2839062/,如需轉載,請註明出處,否則將追究法律責任。

相關文章