python多程式基礎

dwzb發表於2019-02-28

本文首發於知乎
python中的多程式程式設計方式和多執行緒非常相似,幾乎可以說只是換了一些函式,有了之前講過的多執行緒基礎,很多地方我就只展示一些程式碼,在涉及到差別的地方再著重說明。

本文分為如下幾個部分

  • 事先說明
  • 最簡單的多程式
  • 類的形式
  • 程式池
  • 程式之間記憶體獨立
  • 佇列
  • pipe
  • value
  • 程式鎖

事先說明

有兩點在寫程式碼時需要注意

  • 使用多程式時,最好在檔案中編寫程式碼,用cmd來執行,在jupyter經常無法得到想要的結果
  • 建立程式的程式碼一定要放在if __name__ == `__main__`裡面

最簡單的多程式

import multiprocessing
import time
def myfun(num):
time.sleep(1)
print(num + 1)
if __name__ == `__main__`:
for i in range(5):
p = multiprocessing.Process(target = myfun, args = (i, ))
p.start()
複製程式碼

另外,join is_alive daemon name current_process等也都是一樣的。

類的形式

import multiprocessing
import requests
from bs4 import BeautifulSoup
class MyProcess(multiprocessing.Process):
def __init__(self, i):
multiprocessing.Process.__init__(self)
self.i = i
def run(self):
url = `https://movie.douban.com/top250?start={}&filter=`.format(self.i*25)
r = requests.get(url)
soup = BeautifulSoup(r.content, `html.parser`)
lis = soup.find(`ol`, class_=`grid_view`).find_all(`li`)
for li in lis:
title = li.find(`span`, class_="title").text
print(title)
if __name__ == `__main__`:
for i in range(10):
p = MyProcess(i)
p.start()
複製程式碼

程式池

import requests
from bs4 import BeautifulSoup
from multiprocessing import Pool, current_process
def get_title(i):
print(`start`, current_process().name)
title_list = []
url = `https://movie.douban.com/top250?start={}&filter=`.format(i*25)
r = requests.get(url)
soup = BeautifulSoup(r.content, `html.parser`)
lis = soup.find(`ol`, class_=`grid_view`).find_all(`li`)
for li in lis:
title = li.find(`span`, class_="title").text
# return title
title_list.append(title)
print(title)
return(title_list)
if __name__ == `__main__`:
pool = Pool()
for i in range(10):
pool.apply_async(get_title, (i, ))
pool.close()
pool.join()
print(`finish`)
複製程式碼

這裡要說明一下

  • 使用Pool時,不指定程式數量,則預設為CPU核心數量
  • 核心數量對應電腦的(工作管理員-效能)邏輯處理器數量而不是核心數量(我的電腦2個核心,有4個邏輯處理器,所以這裡預設使用4個程式)
  • 程式數量可以是成百上千,並不是說最大開啟程式數量為4,只要用Pool(10)就可以同時開啟10個程式進行抓取
  • 不過要注意一點,無論多執行緒還是多程式,數量開啟太多都會造成切換費時,降低效率,所以慎重建立太多執行緒與程式

程式之間記憶體獨立

多程式與多執行緒最大的不同在於,多程式的每一個程式都有一份變數的拷貝,程式之間的操作互不影響,我們先來看看下面的例子

import multiprocessing
import time
zero = 0
def change_zero():
global zero
for i in range(3):
zero = zero + 1
print(multiprocessing.current_process().name, zero)
if __name__ == `__main__`:
p1 = multiprocessing.Process(target = change_zero)
p2 = multiprocessing.Process(target = change_zero)
p1.start()
p2.start()
p1.join()
p2.join()
print(zero)
複製程式碼

執行結果如下

Process-1 1
Process-1 2
Process-1 3
Process-2 1
Process-2 2
Process-2 3
0
複製程式碼

上面結果顯示,新建立的兩個程式各自把值增加到了3,二者不是一起將其加到了6的。同時,主程式的值還是0。所以說每個程式都是將資料拷貝過去自己做,並沒有將結果與其他程式共享。

但是對於寫入檔案則不同

import multiprocessing
import time
def write_file():
for i in range(30):
with open(`try.txt`, `a`) as f:
f.write(str(i) + ` `)
if __name__ == `__main__`:
p1 = multiprocessing.Process(target = write_file)
p2 = multiprocessing.Process(target = write_file)
p1.start()
p2.start()
p1.join()
p2.join()
複製程式碼

得到的try.txt檔案內容如下

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 0 15 2 16 17 3 4 18 19 5 20 6 21 22 8 9 23 10 11 25 26 12 13 27 28 14 29 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 
複製程式碼

可見兩個程式都將資料寫入了同一份檔案中。

下面我們要討論第一種情況,如果真的要在兩個程式之間共享變數需要怎麼辦

佇列

這裡介紹程式之間的第一種交流方式——佇列。multiprocessing模組中提供了multiprocessing.Queue,它和Queue.Queue的區別在於,它裡面封裝了程式之間的資料交流,不同程式可以操作同一個multiprocessing.Queue

from multiprocessing import Process, Queue
def addone(q):
q.put(1)
def addtwo(q):
q.put(2)
if __name__ == `__main__`:
q = Queue()
p1 = Process(target=addone, args = (q, ))
p2 = Process(target=addtwo, args = (q, ))
p1.start()
p2.start()
p1.join()
p2.join()
print(q.get())
print(q.get())
複製程式碼

執行結果如下

1
2
複製程式碼

這個佇列是執行緒、程式安全的,即對佇列的每一次修改中間不會被中斷從而造成結果錯誤。

pipe

pipe的功能和Queue類似,可以理解成簡化版的Queue。我們先來看下面一個例子

import random
import time
from multiprocessing import Process, Pipe, current_process
def produce(conn):
while True:
new = random.randint(0, 100)
print(`{} produce {}`.format(current_process().name, new))
conn.send(new)
time.sleep(random.random())
def consume(conn):
while True:
print(`{} consume {}`.format(current_process().name, conn.recv()))
time.sleep(random.random())
if __name__ == `__main__`:
pipe = Pipe()
p1 = Process(target=produce, args=(pipe[0],))
p2 = Process(target=consume, args=(pipe[1],))
p1.start()
p2.start()
複製程式碼

結果如下

Process-1 produce 24
Process-2 consume 24
Process-1 produce 95
Process-2 consume 95
Process-1 produce 100
Process-2 consume 100
Process-1 produce 28
Process-2 consume 28
Process-1 produce 62
Process-2 consume 62
Process-1 produce 92
Process-2 consume 92
....................
複製程式碼

上面使用了pipe來實現生產消費模式。

總結Queuepipe之間的差別如下

  • Queue使用put get來維護佇列,pipe使用send recv來維護佇列
  • pipe只提供兩個端點,而Queue沒有限制。這就表示使用pipe時只能同時開啟兩個程式,可以像上面一樣,一個生產者一個消費者,它們分別對這兩個端點(Pipe()返回的兩個值)操作,兩個端點共同維護一個佇列。如果多個程式對pipe的同一個端點同時操作,就會發生錯誤(因為沒有上鎖,類似執行緒不安全)。所以兩個端點就相當於只提供兩個程式安全的操作位置,以此限制了程式數量只能是2
  • Queue的封裝更好,Queue只提供一個結果,它可以被很多程式同時呼叫;而Pipe()返回兩個結果,要分別被兩個程式呼叫
  • Queue的實現基於pipe,所以pipe的執行速度比Queue快很多
  • 當只需要兩個程式時使用pipe更快,當需要多個程式同時操作佇列時,使用Queue

value

當我們不是想維護一個佇列,而只是多個程式同時操作一個數字,就需要提供一個可以在多個程式之間共享的方法,即Value

from multiprocessing import Process, Value
def f1(n):
n.value += 1
def f2(n):
n.value -= 2
if __name__ == `__main__`:
num = Value(`d`, 0.0)
p1 = Process(target=f1, args=(num, ))
p2 = Process(target=f2, args=(num, ))
p1.start()
p2.start()
p1.join()
p2.join()
print(num.value)
複製程式碼

執行結果為

-1.0
複製程式碼

其中Value(`d`, 0.0)中的d表示雙精度浮點數,更多型別可以看這裡

除了Value,模組還提供了類似的Array,感興趣的讀者可以去官網檢視用法

程式鎖

既然變數在程式之間可以共享了,那麼同時操作一個變數導致的不安全也隨之出現。同多執行緒一樣,程式也是通過鎖來解決,而且使用方法都和多執行緒裡相同。

lock = multiprocessing.Lock()
lock.acquire()
lock.release()
with lock:
複製程式碼

這些用法和功能都和多執行緒是一樣的

另外,multiprocessing.Semaphore Condition Event RLock也和多執行緒相同

歡迎關注我的知乎專欄

專欄主頁:python程式設計

專欄目錄:目錄

版本說明:軟體及包版本說明

相關文章