python 併發編程之基礎知識

1. 基礎知識

1.1 進程

  • 進程就是操作系統中執行的一個程序,操作系統以進程爲單位分配存儲空間,每個進程都有自己的地址空間、數據棧以及其他用於跟蹤進程執行的輔助數據,操作系統管理所有進程的執行,爲它們合理的分配資源。
  • 進程可以通過fork或spawn的方式來創建新的進程來執行其他的任務,不過新的進程也有自己獨立的內存空間,因此必須通過進程間通信機制(IPC,Inter-Process Communication)來實現數據共享,具體的方式包括管道、信號、套接字、共享內存區等。

1.2 線程

  • 一個進程還可以擁有多個併發的執行線索,簡單的說就是擁有多個可以獲得CPU調度的執行單元,這就是所謂的線程。
  • 由於線程在同一個進程下,它們可以共享相同的上下文,因此相對於進程而言,線程間的信息共享和通信更加容易。
  • 在單核CPU系統中,真正的併發是不可能的,因爲在某個時刻能夠獲得CPU的只有唯一的一個線程,多個線程共享了CPU的執行時間。
  • 使用多線程實現併發編程爲程序帶來的好處是不言而喻的,最主要的體現在提升程序的性能和改善用戶體驗,今天我們使用的軟件幾乎都用到了多線程技術,這一點可以利用系統自帶的進程監控工具(如macOS中的“活動監視器”、Windows中的“任務管理器”)來證實
  • 當然多線程也並不是沒有壞處,站在其他進程的角度,多線程的程序對其他程序並不友好,因爲它佔用了更多的CPU執行時間,導致其他程序無法獲得足夠的CPU執行時間;
  • 另一方面,站在開發者的角度,編寫和調試多線程的程序都對開發者有較高的要求,對於初學者來說更加困難。

1.3 總結

  • Python既支持多進程又支持多線程,因此使用Python實現併發編程主要有3種方式:多進程、多線程、多進程+多線程。
  • Unix和Linux操作系統上提供了fork()系統調用來創建進程,調用fork()函數的是父進程,創建出的是子進程,子進程是父進程的一個拷貝,但是子進程擁有自己的PID。
  • fork()函數非常特殊它會返回兩次,父進程中可以通過fork()函數的返回值得到子進程的PID,而子進程中的返回值永遠都是0。
  • Python的os模塊提供了fork()函數。
  • 由於Windows系統沒有fork()調用,因此要實現跨平臺的多進程編程,可以使用multiprocessing模塊的Process類來創建子進程,而且該模塊還提供了更高級的封裝,例如批量啓動進程的進程池(Pool)、用於進程間通信的隊列(Queue)和管道(Pipe)等。

1.4 協程基礎

  • 在Python語言中,單線程+異步I/O的編程模型稱爲協程,有了協程的支持,就可以基於事件驅動編寫高效的多任務程序。
  • 協程最大的優勢就是極高的執行效率,因爲子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷。
  • 協程的第二個優勢就是不需要多線程的鎖機制,因爲只有一個線程,也不存在同時寫變量衝突,在協程中控制共享資源不用加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多。
  • 如果想要充分利用CPU的多核特性,最簡單的方法是多進程+協程,既充分利用多核,又充分發揮協程的高效率,可獲得極高的性能。

2 多進程編程(適合CPU密集型計算, 更建議用C和C++編寫)

2.1 代碼示例

from multiprocessing import Process  # 引入多線程模塊
from os import getpid
from random import randint
from time import time, sleep


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', ))   # 配置多線程
    p1.start()  # 啓動多線程
    p2 = Process(target=download_task, args=('Peking Hot.avi', ))  # 配置多線程
    p2.start()  # 啓動多線程
    p1.join()  # 等待線程結束
    p2.join() # 等待線程結束
    end = time()
    print('總共耗費了%.2f秒.' % (end - start))


if __name__ == '__main__':
    main()

在上面的代碼中,我們通過Process類創建了進程對象,通過target參數我們傳入一個函數來表示進程啓動後要執行的代碼,後面的args是一個元組,它代表了傳遞給函數的參數。Process對象的start方法用來啓動進程,而join方法表示等待進程執行結束。運行上面的代碼可以明顯發現兩個下載任務“同時”啓動了,而且程序的執行時間將大大縮短,不再是兩個任務的時間總和。

2. 多線程編程(適合I/0密集型)

2.1 代碼併發執行,創建線程並在合適的時候銷燬

import time
from threading import Thread
def countdown(n):
    while n > 0:
        print("T-minus", n)
        n -= 1
        time.sleep(3)

t = Thread(target=countdown, args=(10,), daemon=True)
t.start()
if t.is_alive():
    print('still running')
else:
    print('completed')

2.2 使用繼承方法來實現多線程編程

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從入門到住院.pdf')
    t1.start()
    t2 = DownloadTask('Peking Hot.avi')
    t2.start()
    t1.join()
    t2.join()
    end = time()
    print('總共耗費了%.2f秒.' % (end - start))


if __name__ == '__main__':
    main()

2.3 '鎖’的問題

from time import sleep
from threading import Thread, Lock


class Account(object):

    def __init__(self):
        self._balance = 0
        self._lock = Lock()

    def deposit(self, money):
        # 先獲取鎖才能執行後續的代碼
        self._lock.acquire()
        try:
            new_balance = self._balance + money
            sleep(0.01)
            self._balance = new_balance
        finally:
            # 在finally中執行釋放鎖的操作保證正常異常鎖都能釋放
            self._lock.release()

    @property
    def balance(self):
        return self._balance


class AddMoneyThread(Thread):

    def __init__(self, account, money):
        super().__init__()
        self._account = account
        self._money = money

    def run(self):
        self._account.deposit(self._money)


def main():
    account = Account()
    threads = []
    for _ in range(100):
        t = AddMoneyThread(account, 1)
        threads.append(t)
        t.start()
    for t in threads:
        t.join()
    print('賬戶餘額爲: ¥%d元' % account.balance)


if __name__ == '__main__':
    main()

3. 協程編程方法(python 高性能編程技術)

  • 異步處理:從調度程序的任務隊列中挑選任務,該調度程序以交叉的形式執行這些任務,我們並不能保證任務將以某種順序去執行,因爲執行順序取決於隊列中的一項任務是否願意將CPU處理時間讓位給另一項任務。異步任務通常通過多任務協作處理的方式來實現,由於執行時間和順序的不確定,因此需要通過回調式編程或者future對象來獲取任務執行的結果。Python 3通過asyncio模塊和awaitasync關鍵字(在Python 3.7中正式被列爲關鍵字)來支持異步處理。
"""
異步I/O - async / await
"""
import asyncio


def num_generator(m, n):
    """指定範圍的數字生成器"""
    yield from range(m, n + 1)


async def prime_filter(m, n):
    """素數過濾器"""
    primes = []
    for i in num_generator(m, n):
        flag = True
        for j in range(2, int(i ** 0.5 + 1)):
            if i % j == 0:
                flag = False
                break
        if flag:
            print('Prime =>', i)
            primes.append(i)

        await asyncio.sleep(0.001)
    return tuple(primes)


async def square_mapper(m, n):
    """平方映射器"""
    squares = []
    for i in num_generator(m, n):
        print('Square =>', i * i)
        squares.append(i * i)

        await asyncio.sleep(0.001)
    return squares


def main():
    """主函數"""
    loop = asyncio.get_event_loop()
    future = asyncio.gather(prime_filter(2, 100), square_mapper(1, 100))
    future.add_done_callback(lambda x: print(x.result()))
    loop.run_until_complete(future)
    loop.close()


if __name__ == '__main__':
    main()

說明:上面的代碼使用get_event_loop函數獲得系統默認的事件循環,通過gather函數可以獲得一個future對象,future對象的add_done_callback可以添加執行完成時的回調函數,loop對象的run_until_complete方法可以等待通過future對象獲得協程執行結果。

  • 當程序不需要真正的併發性或並行性,而是更多的依賴於異步處理和回調時,asyncio就是一種很好的選擇。如果程序中有大量的等待與休眠時,也應該考慮asyncio,它很適合編寫沒有實時數據處理需求的Web應用服務器
  • Python還有很多用於處理並行任務的三方庫,例如:joblib、PyMP等。實際開發中,要提升系統的可擴展性和併發性通常有垂直擴展(增加單個節點的處理能力)和水平擴展(將單個節點變成多個節點)兩種做法。
  • 可以通過消息隊列來實現應用程序的解耦合,消息隊列相當於是多線程同步隊列的擴展版本,不同機器上的應用程序相當於就是線程,而共享的分佈式消息隊列就是原來程序中的Queue。
  • 消息隊列(面向消息的中間件)的最流行和最標準化的實現是AMQP(高級消息隊列協議),AMQP源於金融行業,提供了排隊、路由、可靠傳輸、安全等功能,最著名的實現包括:Apache的ActiveMQ、RabbitMQ等
  • 要實現任務的異步化,可以使用名爲Celery的三方庫。Celery是Python編寫的分佈式任務隊列,它使用分佈式消息進行工作,可以基於RabbitMQ或Redis來作爲後端的消息代理。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章