關於我
程式設計界的一名小程式猿,目前在一個創業團隊任team lead,技術棧涉及Android、Python、Java和Go,這個也是我們團隊的主要技術棧。 聯絡:hylinux1024@gmail.com
0x00 使用程式實現併發
上一篇文章介紹了執行緒的使用。然而Python
中由於Global Interpreter Lock
(全域性解釋鎖GIL
)的存在,每個執行緒在在執行時需要獲取到這個GIL
,在同一時刻中只有一個執行緒得到解釋鎖的執行,Python
中的執行緒並沒有真正意義上的併發執行,多執行緒的執行效率也不一定比單執行緒的效率更高。
如果要充分利用現代多核CPU
的併發能力,就要使用multipleprocessing
模組了。
0x01 multipleprocessing
與使用執行緒的threading
模組類似,multipleprocessing
模組提供許多高階API
。最常見的是Pool
物件了,使用它的介面能很方便地寫出併發執行的程式碼。
from multiprocessing import Pool
def f(x):
return x * x
if __name__ == '__main__':
with Pool(5) as p:
# map方法的作用是將f()方法併發地對映到列表中的每個元素
print(p.map(f, [1, 2, 3]))
# 執行結果
# [1, 4, 9]
複製程式碼
關於Pool
下文中還會提到,這裡我們先來看Process
。
Process
要建立一個程式可以使用Process
類,使用start()
方法啟動程式。
from multiprocessing import Process
import os
def echo(text):
# 父程式ID
print("Process Parent ID : ", os.getppid())
# 程式ID
print("Process PID : ", os.getpid())
print('echo : ', text)
if __name__ == '__main__':
p = Process(target=echo, args=('hello process',))
p.start()
p.join()
# 執行結果
# Process Parent ID : 27382
# Process PID : 27383
# echo : hello process
複製程式碼
程式池
正如開篇提到的multiprocessing
模組提供了Pool
類可以很方便地實現一些簡單多程式場景。
它主要有以下介面
apply(func[, args[, kwds]])
執行func(args,kwds)
方法,在方法結束返回前會阻塞。apply_async(func[, args[, kwds[, callback[, error_callback]]]])
非同步執行func(args,kwds)
,會立即返回一個result
物件,如果指定了callback
引數,結果會通過回撥方法返回,還可以指定執行出錯的回撥方法error_callback()
map(func, iterable[, chunksize])
類似內建函式map()
,可以併發執行func
,是同步方法map_async(func, iterable[, chunksize[, callback[, error_callback]]])
非同步版本的map
close()
關閉程式池。當池中的所有工作程式都執行完畢時,程式會退出。terminate()
終止程式池join()
等待工作程式執行完,必需先呼叫close()
或者terminate()
from multiprocessing import Pool
def f(x):
return x * x
if __name__ == '__main__':
with Pool(5) as p:
# map方法的作用是將f()方法併發地對映到列表中的每個元素
a = p.map(f, [1, 2, 3])
print(a)
# 非同步執行map
b = p.map_async(f, [3, 5, 7, 11])
# b 是一個result物件,代表方法的執行結果
print(b)
# 為了拿到結果,使用join方法等待池中工作程式退出
p.close()
# 呼叫join方法前,需先執行close或terminate方法
p.join()
# 獲取執行結果
print(b.get())
# 執行結果
# [1, 4, 9]
# <multiprocessing.pool.MapResult object at 0x10631b710>
# [9, 25, 49, 121]
複製程式碼
map_async()
和apply_async()
執行後會返回一個class multiprocessing.pool.AsyncResult
物件,通過它的get()
可以獲取到執行結果,ready()
可以判斷AsyncResult
的結果是否準備好。
程式間資料的傳輸
multiprocessing
模組提供了兩種方式用於程式間的資料共享:佇列(Queue
)和管道(Pipe
)
Queue
是執行緒安全,也是程式安全的。使用Queue
可以實現程式間的資料共享,例如下面的demo
中子程式put
一個物件,在主程式中就能get
到這個物件。
任何可以序列化的物件都可以通過Queue
來傳輸。
from multiprocessing import Process, Queue
def f(q):
q.put([42, None, 'hello'])
if __name__ == '__main__':
# 使用Queue進行資料通訊
q = Queue()
p = Process(target=f, args=(q,))
p.start()
# 主程式取得子程式中的資料
print(q.get()) # prints "[42, None, 'hello']"
p.join()
# 執行結果
# [42, None, 'hello']
複製程式碼
Pipe()
返回一對通過管道連線的Connection
物件。這兩個物件可以理解為管道的兩端,它們通過send()
和recv()
傳送和接收資料。
from multiprocessing import Process, Pipe
def write(conn):
# 子程式中傳送一個物件
conn.send([42, None, 'hello'])
conn.close()
def read(conn):
# 在讀的程式中通過recv接收物件
data = conn.recv()
print(data)
if __name__ == '__main__':
# Pipe()方法返回一對連線物件
w_conn, r_conn = Pipe()
wp = Process(target=write, args=(w_conn,))
rp = Process(target=read, args=(r_conn,))
wp.start()
rp.start()
# 執行結果
# [42, None, 'hello']
複製程式碼
需要注意的是,兩個程式不能同時對一個連線物件進行send
或recv
操作。
同步
我們知道執行緒間的同步是通過鎖機制來實現的,程式也一樣。
from multiprocessing import Process, Lock
import time
def print_with_lock(l, i):
l.acquire()
try:
time.sleep(1)
print('hello world', i)
finally:
l.release()
def print_without_lock(i):
time.sleep(1)
print('hello world', i)
if __name__ == '__main__':
lock = Lock()
# 先執行有鎖的
for num in range(5):
Process(target=print_with_lock, args=(lock, num)).start()
# 再執行無鎖的
# for num in range(5):
# Process(target=print_without_lock, args=(num,)).start()
複製程式碼
有鎖的程式碼將每秒依次列印
hello world 0
hello world 1
hello world 2
hello world 3
hello world 4
複製程式碼
如果執行無鎖的程式碼,則在我的電腦上執行結果是這樣的
hello worldhello world 0
1
hello world 2
hello world 3
hello world 4
複製程式碼
除了Lock
,還包括RLock
、Condition
、Semaphore
和Event
等程式間的同步原語。其用法也與執行緒間的同步原語很類似。API
使用可以參考文末中引用的文件連結。
在工程中實現程式間的資料共享應當優先使用佇列或管道。
0x02 總結
本文對multiprocessing
模組中常見的API
作了簡單的介紹。講述了Process
和Pool
的常見用法,同時介紹了程式間的資料方式:佇列和管道。最後簡單瞭解了程式間的同步原語。
通過與上篇的對比學習,本文的內容應該是更加容易掌握的。