Python程式和執行緒例項詳解

Huny發表於2021-06-23

前言

程式是什麼?

程式就是一個程式在一個資料集上的一次動態執行過程。程式一般由程式、資料集、程式控制塊三部分組成。我們編寫的程式用來描述程式要完成哪些功能以及如何完成;資料集則是程式在執行過程中所需要使用的資源;程式控制塊用來記錄程式的外部特徵,描述程式的執行變化過程,系統可以利用它來控制和管理程式,它是系統感知程式存在的唯一標誌。

執行緒是什麼?

執行緒也叫輕量級程式,它是一個基本的CPU執行單元,也是程式執行過程中的最小單元,由執行緒ID、程式計數器、暫存器集合和堆疊共同組成。執行緒的引入減小了程式併發執行時的開銷,提高了作業系統的併發效能。執行緒沒有自己的系統資源。

程式和執行緒的區別

程式是計算機中的程式關於某資料集合上的一次執行活動,是系統進行資源分配和排程的基本單位,是作業系統結構的基礎。或者說程式是具有一定獨立功能的程式關於某個資料集合上的一次執行活動,程式是系統進行資源分配和排程的一個獨立單位。
執行緒則是程式的一個實體,是CPU排程和分派的基本單位,它是比程式更小的能獨立執行的基本單位。

程式和執行緒的關係:
(1)一個執行緒只能屬於一個程式,而一個程式可以有多個執行緒,但至少有一個執行緒。
(2)資源分配給程式,同一程式的所有執行緒共享該程式的所有資源。
(3)CPU分給執行緒,即真正在CPU上執行的是執行緒。

並行和併發

並行處理(Parallel Processing)是計算機系統中能同時執行兩個或者更多個處理的一種計算方法。並行處理可同時工作於同一程式的不同方面,並行處理的主要目的是節省大型和複雜問題的解決時間。

併發處理(concurrency Processing)是指一個時間段中有幾個程式都處於已經啟動執行到執行完畢之間,而且這幾個程式都是在同一處理機(CPU)上執行,但任意時刻點上只有一個程式在處理機(CPU)上執行

同步和非同步

同步就是指一個程式在執行某個請求的時候,若該請求需要一段時間才能返回資訊,那麼這個程式將會一直等待下去,直到收到返回資訊才繼續執行下去;
非同步是指程式不需要一直等下去,而是繼續執行下面的操作,不管其他程式的狀態。當有訊息返回時系統會通知程式進行處理,這樣可以提高執行的效率。
舉個例子,打電話時就是同步通訊,發短息時就是非同步通訊。

單例執行

from random import randint
from time import time, sleep


def download_task(filename):
    print('開始下載%s...' % filename)
    time_to_download = randint(5, 10)
    sleep(time_to_download)
    print('%s下載完成! 耗費了%d秒' % (filename, time_to_download))


def main():
    start = time()
    download_task('Python入門.pdf')
    download_task('av.avi')
    end = time()
    print('總共耗費了%.2f秒.' % (end - start))


if __name__ == '__main__':
    main()

執行是順序執行,所以耗時是多個程式的時間總和

因為是單程式任務,所有任務都是排隊進行所以這樣執行效率非常的低。我們來新增多程式模式進行多程式同時執行,這樣一個程式執行時,另一個程式無需等待,執行時間將大大縮短。

多程式

from random import randint
from time import time, sleep
from multiprocessing import Process
from os import getpid


def download_task(filename):
    print('啟動下載程式,程式號:[%d]'%getpid())
    print('開始下載%s...' % filename)
    time_to_download = randint(5, 10)
    sleep(time_to_download)
    print('%s下載完成! 耗費了%d秒' % (filename, time_to_download))


def main():
    start = time()
    p1 = Process(target=download_task,args=('python入門.pdf',))
    p2 = Process(target=download_task,args=('av.avi',))
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    # download_task('Python入門.pdf')
    # download_task('av.avi')
    end = time()
    print('總共耗費了%.2f秒.' % (end - start))


if __name__ == '__main__':
    main()

多個程式並排執行,總耗時就是最長耗時的那個程式的時間。

大致的執行流程如下圖

多程式的特點是相互獨立,不會共享全域性變數,即在一個程式中對全域性變數修改過後,不會影響另一個程式中的全域性變數。

程式間通訊

from random import randint
from time import time,sleep
from multiprocessing import Process
from os import getpid

time_to_download = 3
def download_task(filename):
    global time_to_download
    time_to_download += 1
    print('啟動下載程式,程式號:[%d]'%getpid())
    print('開始下載%s...' % filename)
    sleep(time_to_download)
    print('%s下載完成! 耗費了%d秒' % (filename, time_to_download))

def download_task2(filename):
    global time_to_download
    print('啟動下載程式,程式號:[%d]'%getpid())
    print('開始下載%s...' % filename)
    sleep(time_to_download)
    print('%s下載完成! 耗費了%d秒' % (filename, time_to_download))

def main():
    start = time()
    p1 = Process(target=download_task,args=('python入門.pdf',))
    p2 = Process(target=download_task2,args=('av.avi',))
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    end = time()
    print('總共耗費了%.2f秒.' % (end - start))


if __name__ == '__main__':
    main()

從執行結果可以看出,兩個程式間的全域性變數無法共享,所以它們是相互獨立的

當然多程式也是可以進行通過一些方法進行資料共享的。可以使用multiprocessing模組的Queue實現多程式之間的資料傳遞,Queue本身是一個訊息列隊程式。

這裡介紹Queue的常用程式通訊的兩種方法:
put 方法用以插入資料到佇列中, put 方法還有兩個可選引數: blocked 和 timeout。如果 blocked 為 True(預設值),並且 timeout 為正值,該方法會阻塞 timeout 指定的時間,直到該佇列有剩餘的空間。如果超時,會丟擲 Queue.full 異常。如果 blocked 為 False,但該 Queue 已滿,會立即丟擲 Queue.full 異常。

get 方法可以從佇列讀取並且刪除一個元素。同樣, get 方法有兩個可選引數: blocked和 timeout。如果 blocked 為 True(預設值),並且 timeout 為正值,那麼在等待時間內沒有取到任何元素,會丟擲 Queue.Empty 異常。如果 blocked 為 False,有兩種情況存在,如果Queue 有一個值可用,則立即返回該值,否則,如果佇列為空,則立即丟擲Queue.Empty 異常。
Queue 佇列實現程式間通訊

from random import randint
from time import time,sleep
from multiprocessing import Process
import multiprocessing
from os import getpid

time_to_download = 3
def write(q):
    for i in ['python入門','av.avi','java入門']:
        q.put(i)
        print('啟動寫入程式,程式號:[%d]'%getpid())
        print('開始寫入%s...' % i)  
        sleep(time_to_download)

def read(q):
    while True:
        if not q.empty():
            print('啟動讀取程式,程式號:[%d]'%getpid())
            print('開始讀取%s...' % q.get())
            sleep(time_to_download)
        else:
            break

def main():
    q = multiprocessing.Queue()
    p1 = Process(target=write,args=(q,))
    p2 = Process(target=read,args=(q,))
    p1.start()
    p1.join()
    p2.start()
    p2.join()


if __name__ == '__main__':
    main()

上一個程式寫入的資料通過Queue佇列共享給了下一個程式,然後下一個程式可以直接進行使用,這樣就完成了多程式間的資料共享。

程式池

Pool類可以提供指定數量的程式供使用者呼叫,當有新的請求提交到Pool中時,如果池還沒有滿,就會建立一個新的程式來執行請求。如果池滿,請求就會告知先等待,直到池中有程式結束,才會建立新的程式來執行這些請求。
程式池中常見三個方法:
◆apply:序列
◆apply_async:並行
◆map

多執行緒

from random import randint
from time import time, sleep
from threading import Thread
from os import getpid

def download_task(filename):
    print('啟動下載程式,程式號:[%d]' % getpid())
    print('開始下載%s...' % filename)
    time_to_download = randint(5, 10)
    sleep(time_to_download)
    print('%s下載完成! 耗費了%d秒' % (filename, time_to_download))

def main():
    start = time()
    p1 = Thread(target=download_task, args=('python入門.pdf',))
    p2 = Thread(target=download_task, args=('av.avi',))
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    end = time()
    print('總共耗費了%.2f秒.' % (end - start))

if __name__ == '__main__':
    main()

多執行緒執行因為GIL鎖的存在,實際上執行是進行單執行緒,即一次只執行一個執行緒,然後在切換其他的執行緒進行執行,因為其中切換的時間非常的短,所以看上去依然像是多執行緒一起執行。

通過繼承Thread類的方式來建立自定義的執行緒類,然後再建立執行緒物件並啟動執行緒

from random import randint
from threading import Thread
from time import time, sleep

class DownloadTask(Thread):
    def __init__(self, filename):
        super().__init__()
        self._filename = filename

    def run(self):
        print('開始下載%s...'% self._filename)
        time_to_download = randint(5,10)
        sleep(time_to_download)
        print('%s下載完成!耗費了%d秒' %(self._filename, time_to_download))

def main():
    start = time()
    t1 = DownloadTask('python入門')
    t2 = DownloadTask('av.avi')
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    end = time()
    print('共耗費了%.2f秒'%(end - start))

if __name__ == '__main__':
    main()

多執行緒使用類還是函式執行的結果完全一致,具體怎麼使用可以結合自己的使用場景。

相關文章