Python的多程式和多執行緒

酸蘿蔔別吃發表於2021-03-28

程式和執行緒

程式是系統進行資源分配的最小單位,執行緒是系統進行排程執行的最小單位;

一個應用程式至少包含一個程式,一個程式至少包含一個執行緒;

每個程式在執行過程中擁有獨立的記憶體空間,而一個程式中的執行緒之間是共享該程式的記憶體空間的;

  • 計算機的核心是CPU,它承擔了所有的計算任務。它就像一座工廠,時刻在執行。
  • 假定工廠的電力有限,一次只能供給一個車間使用。也就是說,一個車間開工的時候,其他車間都必須停工。背後的含義就是,單個CPU一次只能執行一個任務。編者注: 多核的CPU就像有了多個發電廠,使多工廠(多程式)實現可能。
  • 程式就好比工廠的車間,它代表CPU所能處理的單個任務。任一時刻,CPU總是執行一個程式,其他程式處於非執行狀態。
  • 一個車間裡,可以有很多工人。他們協同完成一個任務。
  • 執行緒就好比車間裡的工人。一個程式可以包括多個執行緒。
  • 車間的空間是工人們共享的,比如許多房間是每個工人都可以進出的。這象徵一個程式的記憶體空間是共享的,每個執行緒都可以使用這些共享記憶體。
  • 可是,每間房間的大小不同,有些房間最多隻能容納一個人,比如廁所。裡面有人的時候,其他人就不能進去了。這代表一個執行緒使用某些共享記憶體時,其他執行緒必須等它結束,才能使用這一塊記憶體。
  • 一個防止他人進入的簡單方法,就是門口加一把鎖。先到的人鎖上門,後到的人看到上鎖,就在門口排隊,等鎖開啟再進去。這就叫"互斥鎖"(Mutual exclusion,縮寫 Mutex),防止多個執行緒同時讀寫某一塊記憶體區域。
  • 還有些房間,可以同時容納n個人,比如廚房。也就是說,如果人數大於n,多出來的人只能在外面等著。這好比某些記憶體區域,只能供給固定數目的執行緒使用。
  • 這時的解決方法,就是在門口掛n把鑰匙。進去的人就取一把鑰匙,出來時再把鑰匙掛回原處。後到的人發現鑰匙架空了,就知道必須在門口排隊等著了。這種做法叫做"訊號量"(Semaphore),用來保證多個執行緒不會互相沖突。
  • 不難看出,mutex是semaphore的一種特殊情況(n=1時)。也就是說,完全可以用後者替代前者。但是,因為mutex較為簡單,且效率高,所以在必須保證資源獨佔的情況下,還是採用這種設計。

Python的多程式

Python的多程式依賴於multiprocess模組;使用多程式可以利用多個CPU進行平行計算;

例項:

from multiprocessing import Process
import os
import time

def long_time_task(i):
    print('子程式: {} - 任務{}'.format(os.getpid(), i))
    time.sleep(2)
    print("結果: {}".format(8 ** 20))

if __name__=='__main__':
    print('當前母程式: {}'.format(os.getpid()))
    start = time.time()
    p1 = Process(target=long_time_task, args=(1,))
    p2 = Process(target=long_time_task, args=(2,))
    print('等待所有子程式完成。')
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    end = time.time()
    print("總共用時{}秒".format((end - start)))

新建立程式和程式間切換是需要消耗資源的,所以應該控制程式數量;

同時可執行的程式數量收到CPU核數限制;

程式池

使用程式池pool建立程式:

使用程式池可以避免手工進行程式的建立的麻煩,預設數量是CPU核數;

Pool類可以提供指定數量的程式供使用者使用,當有新的請求被提交到Pool中的時候,如果程式池還沒有滿,就會建立一個新的程式來執行請求;如果池已經滿了,請求就會等待,等到有空閒程式可以使用時,才會執行請求;

幾個方法:

1.apply_async

作用是向程式池提交需要執行的函式和引數,各個程式採用非阻塞的非同步方式呼叫,每個程式只管自己執行,是預設方式;

2.map

會阻塞程式直到返回結果;

3.map_sunc

非阻塞程式;

4.close

關閉程式池,不再接受任務;

5.terminate

結束程式;

6.join

主程式阻塞,直到子程式執行結束;

例項:

from multiprocessing import Pool, cpu_count
import os
import time

def long_time_task(i):
    print('子程式: {} - 任務{}'.format(os.getpid(), i))
    time.sleep(2)
    print("結果: {}".format(8 ** 20))

if __name__=='__main__':
    print("CPU核心數:{}".format(cpu_count()))
    print('當前母程式: {}'.format(os.getpid()))
    start = time.time()
    p = Pool(4)
    for i in range(5):
        p.apply_async(long_time_task, args=(i,))
    print('等待所有子程式完成。')
    p.close()
    p.join()
    end = time.time()
    print("總共用時{}秒".format((end - start)))

在join之前,必須使用close或者terminate,讓程式池不再接受任務;

多程式間的資料通訊與共享

通常,程式之間是相互獨立的,每個程式都有獨立的記憶體。通過共享記憶體(nmap模組),程式之間可以共享物件,使多個程式可以訪問同一個變數(地址相同,變數名可能不同)。多程式共享資源必然會導致程式間相互競爭,所以應該盡最大可能防止使用共享狀態。還有一種方式就是使用佇列queue來實現不同程式間的通訊或資料共享,這一點和多執行緒程式設計類似。

下例這段程式碼中中建立了2個獨立程式,一個負責寫(pw), 一個負責讀(pr), 實現了共享一個佇列queue。

from multiprocessing import Process, Queue
import os, time, random

# 寫資料程式執行的程式碼:
def write(q):
    print('Process to write: {}'.format(os.getpid()))
    for value in ['A', 'B', 'C']:
        print('Put %s to queue...' % value)
        q.put(value)
        time.sleep(random.random())

# 讀資料程式執行的程式碼:
def read(q):
    print('Process to read:{}'.format(os.getpid()))
    while True:
        value = q.get(True)
        print('Get %s from queue.' % value)

if __name__=='__main__':
    # 父程式建立Queue,並傳給各個子程式:
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))
    # 啟動子程式pw,寫入:
    pw.start()
    # 啟動子程式pr,讀取:
    pr.start()
    # 等待pw結束:
    pw.join()
    # pr程式裡是死迴圈,無法等待其結束,只能強行終止:
    pr.terminate()

Python的多執行緒

python 3中的多程式程式設計主要依靠threading模組。建立新執行緒與建立新程式的方法非常類似。threading.Thread方法可以接收兩個引數, 第一個是target,一般指向函式名,第二個時args,需要向函式傳遞的引數。對於建立的新執行緒,呼叫start()方法即可讓其開始。我們還可以使用current_thread().name列印出當前執行緒的名字。 

import threading
import time

def long_time_task(i):
    print('當前子執行緒: {} 任務{}'.format(threading.current_thread().name, i))
    time.sleep(2)
    print("結果: {}".format(8 ** 20))

if __name__=='__main__':
    start = time.time()
    print('這是主執行緒:{}'.format(threading.current_thread().name))
    thread_list = []
    for i in range(1, 3):
        t = threading.Thread(target=long_time_task, args=(i, ))
        thread_list.append(t)
    for t in thread_list:
        t.start()
    for t in thread_list:
        t.join()
    end = time.time()
    print("總共用時{}秒".format((end - start)))

多執行緒間的資料共享

一個程式所含的不同執行緒間共享記憶體,這就意味著任何一個變數都可以被任何一個執行緒修改,因此執行緒之間共享資料最大的危險在於多個執行緒同時改一個變數,把內容給改亂了。如果不同執行緒間有共享的變數,其中一個方法就是在修改前給其上一把鎖lock,確保一次只有一個執行緒能修改它。threading.lock()方法可以輕易實現對一個共享變數的鎖定,修改完後release供其它執行緒使用。

import threading

class Account:
    def __init__(self):
        self.balance = 0

    def add(self, lock):
        # 獲得鎖
        lock.acquire()
        for i in range(0, 100000):
            self.balance += 1
        # 釋放鎖
        lock.release()

    def delete(self, lock):
        # 獲得鎖
        lock.acquire()
        for i in range(0, 100000):
            self.balance -= 1
            # 釋放鎖
        lock.release()

if __name__ == "__main__":
    account = Account()
    lock = threading.Lock()
    # 建立執行緒
   thread_add = threading.Thread(target=account.add, args=(lock,), name='Add')
    thread_delete = threading.Thread(target=account.delete, args=(lock,), name='Delete')

    # 啟動執行緒
   thread_add.start()
    thread_delete.start()

    # 等待執行緒結束
   thread_add.join()
    thread_delete.join()

    print('The final balance is: {}'.format(account.balance))

使用queue佇列通訊-經典的生產者和消費者模型

from queue import Queue
import random, threading, time

# 生產者類
class Producer(threading.Thread):
    def __init__(self, name, queue):
        threading.Thread.__init__(self, name=name)
        self.queue = queue

    def run(self):
        for i in range(1, 5):
            print("{} is producing {} to the queue!".format(self.getName(), i))
            self.queue.put(i)
            time.sleep(random.randrange(10) / 5)
        print("%s finished!" % self.getName())

# 消費者類
class Consumer(threading.Thread):
    def __init__(self, name, queue):
        threading.Thread.__init__(self, name=name)
        self.queue = queue

    def run(self):
        for i in range(1, 5):
            val = self.queue.get()
            print("{} is consuming {} in the queue.".format(self.getName(), val))
            time.sleep(random.randrange(10))
        print("%s finished!" % self.getName())

def main():
    queue = Queue()
    producer = Producer('Producer', queue)
    consumer = Consumer('Consumer', queue)

    producer.start()
    consumer.start()

    producer.join()
    consumer.join()
    print('All threads finished!')

if __name__ == '__main__':
    main()

 

  • 對CPU密集型程式碼(比如迴圈計算) - 多程式效率更高
  • 對IO密集型程式碼(比如檔案操作,網路爬蟲) - 多執行緒效率更高。

對於IO密集型操作,大部分消耗時間其實是等待時間,在等待時間中CPU是不需要工作的,那你在此期間提供雙CPU資源也是利用不上的,相反對於CPU密集型程式碼,2個CPU幹活肯定比一個CPU快很多。那麼為什麼多執行緒會對IO密集型程式碼有用呢?這時因為python碰到等待會釋放GIL供新的執行緒使用,實現了執行緒間的切換。

相關文章