文章目錄
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
模塊和await
和async
關鍵字(在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來作爲後端的消息代理。