Python 多程式的自定義共享資料型別

weixin_33866037發表於2018-09-26

最近專案要用到Python多程式,程式是1+N式的,1用來向一個資料結構中不斷的寫資料,其他N個程式從同時從這個資料結構中讀資料,每條資料都是一次性的。因此:

  • 當前程式可以作為這裡的1程式,然後預設新建N個子程式。N的大小一般和CPU核數相同,如果是生產環境,不想讓程式佔滿伺服器的資源,可以再設定一個最大程式數。
  • 因為是共享操作,這個資料結構就需要是程式安全的。

在網上查了一些關於Python多程式的資料,發現大多都只是從官方文件中隨便找了些程式碼(有的連改都沒改),都是些不能用來實用的初級知識,實在無法稱其為教程(這裡面就包括我之前寫的Python多程式文章)。經過我不斷的篩選和閱讀官方文件,終於讓我找到了一個可以用的方法。

這裡先列出我找到的對我有幫助的文章列表:

正式寫程式碼之前,先要說清楚幾個點。

理解多程式

這點可以參考下我之前寫的文章執行緒、程式、協程。對於一個程式來說,如果其呼叫了join(),它會阻塞自己,一直要等到其他程式都退出後才會繼續執行下去。

這就解決了我的第一個問題,就是讓主程式作為1程式不斷寫入資料,具體的操作方法就是在新建子程式時不呼叫其join()方法。

由淺入深的幾個例子

multiprocessing庫提供了兩種方式建立子程式Process類和Pool類,前者需要對每個子程式進行操作,包括通訊、同步和共享;後者則是對前者的封裝,其維護了一定的程式池並實現了非同步操作。當然還有一些其他的操作,這裡就不贅述了。

場景

我這裡需要的共享資料結構是一個最小堆,堆中的儲存的是一條條記錄(大概30W條),並按照某個記錄中的時間戳排序。1程式會定時重新整理這個堆,N程式會不斷從堆中pop資料,處理後根據新的時間戳再push進堆中。

這其中其實涉及到了幾個點:建立多程式、給子程式傳入引數、自定義堆結構、共享鎖。這裡我一一解釋。

建立子程式並傳入引數的方式

前面說了,建立子程式一般來說有兩種方式:Process和Pool,這裡我用了Process。為什麼不用Pool呢?Pool的優勢是可以使用非同步操作,但這裡有個問題就是普通的multiprocess.Lock不支援Pool,需要使用Manager().Lock()才行,這樣就顯得笨重了,加上我這個專案對非同步要求並不高,就採用了Process類用來建立子程式。

這部分程式碼網上搜一下有很多,我也放出我的測試程式碼:

from multiprocessing import Process
import os


class TestClass(object):
    def __init__(self, *args, **kwargs):
        pass

    def func(self, i):
        print('SubProcess[{}]: {}'.format(i, os.getpid()))

    def start(self):
        print('Main Process: {}, begin'.format(os.getpid()))
        processes = [Process(target=self.func, args=(i,)) for i in range(3)]
        for p in processes:
            p.start()
        for p in processes:
            p.join()
        print('Main Process: {}, end'.format(os.getpid()))


if __name__ == '__main__':
    TestClass().start()

這裡有個地方讓我疑惑了很久,因為官網上有一塊程式碼是這樣的:

from multiprocessing import Process, Lock

def f(l, i):
    l.acquire()
    print 'hello world', i
    l.release()

if __name__ == '__main__':
    lock = Lock()

    for num in range(10):
        Process(target=f, args=(lock, num)).start()

這一度讓我以為只要用Process+Lock就能滿足我的需求了。試了之後發現其實這樣是沒辦法在程式間共享資料的(每個程式都有獨立的PCB,還記得嗎)。這樣只能保證多程式在訪問公共資源時不會產生衝突(比如說標準輸入輸出,也就是上面這個例子)。如果要程式間共享資料,需要使用QueuePipe,或共享記憶體(ValueArray形式),或用Manager(後面會說),不管哪種,都不能滿足我“自定義共享最小堆”的需求。

自定義共享資料

這節和多程式沒關係,主要是講我要用到的這個資料結構。關於堆是什麼我就不解釋了。Python中有個叫heapq的庫,這個庫中,堆中的資料可以是一個tuple,在排序比較的時候會優先比較tuple[0]的內容,如果相等再比較tuple[1]……以此類推。文件中有一個關於優先順序序列的實現很有參考意義,這也要求tuple中的幾個資料必須是可以比較的(物件實現了__lt__, __gt__,__eq__方法)。這裡我要實現的是一個支援自定義比較函式的堆,我稱為HeapQueueWithComparer,直接給出程式碼:

"""支援自定義比較函式(cmp)的堆佇列heap queue"""
import heapq


class HeapQueueWithComparer(object):
    def __init__(self, initial=None, comparer=lambda x: x, heapify=heapq.heapify):
        self.comparer = comparer
        self.data = []

        if initial:
            self.data = [(self.comparer(item), item) for item in initial]
            heapify(self.data)
        else:
            self.data = []

    def push(self, item, heappush=heapq.heappush):
        heappush(self.data, (self.comparer(item), item))

    def pop(self, heappop=heapq.heappop):
        return heappop(self.data)[1] if self.data else None

其原理就是根據comparer生成一個特徵值,然後將其和資料本身作為一個元組在堆中進行排序。用起來的時候可以這麼用:

import random
from collections import namedtuple

volume_t = namedtuple('volume_t', ['length', 'width', 'height', 'id'])

def __get_rand():
    return random.randint(1, 10)

data = [volume_t(__get_rand(), __get_rand(), __get_rand(), i) for i in range(5)]
heap_cmp = HeapQueueWithComparer(data, lambda x: x.length*x.width*x.height)

[print(heap_cmp.pop()) for i in range(len(data))]

這裡是根據一個長方體的體積排序,用namedtuple的原因是使程式碼更可讀,理論上用tuple是一樣的。

接下來要解決的一個問題就是如何讓這個堆再多個程式中共享並且不會產生不一致的問題。前文其實提了,程式間共享資料的一種方式是使用server process,實際中使用就是multiprocessing.managers,它會新建一個額外的程式用來管理共享的資料,而其本身會作為一個代理,每次子程式需要操作共享資料,實際上都是通過這個server process進行操作的,文件中稱之為proxy。一個manager物件支援list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Queue, Value and Array。因此,我們需要自定義一個Manager,並讓它支援帶鎖的操作。這裡提一句,Manager也可以是遠端的,不過這裡用不到。

自定義Manager需要繼承BaseManager,測試程式碼如下。注意這裡和上一個例子中的程式碼並不完全一致,我是覺得再用lambda對程式碼的可讀性影響太大了。:

import heapq
import os
import random
import time
from collections import namedtuple
from multiprocessing import Lock, Process
from multiprocessing.managers import BaseManager


class HeapManager(BaseManager):
    def __init__(self):
        super().__init__()
        self.lock = Lock()
        self.data = []

    def sync(self, rules_changed):
        with self.lock:
            if self.data:
                self.data[:] = []
            self.data = [(item[0]*item[1]*item[2], item,) for item in rules_changed]
            heapq.heapify(self.data)

    def pop(self):
        with self.lock:
            return heapq.heappop(self.data)[1] if self.data else None

    def push(self, item):
        with self.lock:
            heapq.heappush(self.data, (item[0]*item[1]*item[2], item,))


HeapManager.register('HeapManager', HeapManager)
volume_t = namedtuple('volume_t', ['length', 'width', 'height', 'id'])


class TestClass(object):
    def __init__(self, *args, **kwargs):
        manager = HeapManager()
        manager.start()
        self.hm = manager.HeapManager()
        self.lock = Lock()

    def func(self, i):
        while True:
            with self.lock:
                item = self.hm.pop()
            if not item:
                print('Begin to sleep 2s.')
                time.sleep(2)
            else:
                print('Process-{} {} {}'.format(i, os.getpid(), item))

    def start(self):
        jobs = [Process(target=self.func, args=(i, )) for i in range(3)]
        [j.start() for j in jobs]
        # [j.join() for j in jobs] # 不用這行可以不阻塞主程式

        def __get_rand():
            return random.randint(1, 10)

        while True:
            data = [volume_t(__get_rand(), __get_rand(), __get_rand(), i) for i in range(10)]
            with self.lock:
                self.hm.sync(data)
            print('Main {}, add {}.'.format(os.getpid(), data))
            print(sorted(data))
            time.sleep(5)


if __name__ == '__main__':
    TestClass().start()

相關文章