multiprocessing trong python - quy trình forkserver kế thừa điều gì từ quy trình mẹ?

Aug 15 2020

Tôi đang cố gắng sử dụng forkservervà tôi gặp phải NameError: name 'xxx' is not definedtrong các quy trình của công nhân.

Tôi đang sử dụng Python 3.6.4, nhưng tài liệu phải giống nhau, từ https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods nó nói rằng:

Quy trình máy chủ fork là một luồng đơn nên có thể an toàn khi sử dụng os.fork (). Không có tài nguyên không cần thiết nào được kế thừa.

Ngoài ra, nó nói:

Tốt hơn để kế thừa hơn là dưa chua / bỏ chọn

Khi sử dụng các phương pháp khởi động spawn hoặc forkserver, nhiều loại từ quá trình đa xử lý cần phải được chọn để các quy trình con có thể sử dụng chúng . Tuy nhiên, thường nên tránh gửi các đối tượng được chia sẻ đến các quy trình khác bằng cách sử dụng đường ống hoặc hàng đợi. Thay vào đó, bạn nên sắp xếp chương trình để một quy trình cần truy cập vào tài nguyên được chia sẻ được tạo ở nơi khác có thể kế thừa nó từ quy trình tổ tiên.

Vì vậy, rõ ràng một đối tượng chính mà quy trình công nhân của tôi cần làm việc đã không được quy trình máy chủ kế thừa và sau đó chuyển cho công nhân, tại sao điều đó lại xảy ra? Tôi tự hỏi chính xác thì điều gì được quy trình forkserver kế thừa từ quy trình mẹ?

Đây là mã của tôi trông như thế nào:

import multiprocessing
import (a bunch of other modules)

def worker_func(nameList):
    global largeObject
    for item in nameList:
        # get some info from largeObject using item as index
        # do some calculation
        return [item, info]

if __name__ == '__main__':
    result = []
    largeObject # This is my large object, it's read-only and no modification will be made to it.
    nameList # Here is a list variable that I will need to get info for each item in it from the largeObject    
    ctx_in_main = multiprocessing.get_context('forkserver')
    print('Start parallel, using forking/spawning/?:', ctx_in_main.get_context())
    cores = ctx_in_main.cpu_count()
    with ctx_in_main.Pool(processes=4) as pool:
        for x in pool.imap_unordered(worker_func, nameList):
            result.append(x)

Cảm ơn bạn!

Tốt,

Trả lời

1 alex_noname Aug 16 2020 at 19:32

Học thuyết

Dưới đây là một đoạn trích từ blog Bojan Nikolic

Các phiên bản Python hiện đại (trên Linux) cung cấp ba cách để bắt đầu các quy trình riêng biệt:

  1. Fork () -ing các quy trình cha và tiếp tục với hình ảnh các quy trình giống nhau trong cả cha và con. Phương pháp này nhanh, nhưng có khả năng không đáng tin cậy khi trạng thái gốc phức tạp

  2. Tạo ra các quy trình con, tức là fork () - ing và sau đó thực thi để thay thế hình ảnh quy trình bằng một quy trình Python mới. Phương pháp này đáng tin cậy nhưng chậm, vì hình ảnh quy trình được tải lại sau khi tải.

  3. chế forkserver , bao gồm một máy chủ Python riêng biệt có trạng thái tương đối đơn giản và là fork () - ed khi cần một quy trình mới. Phương thức này kết hợp tốc độ của Fork () - ing với độ tin cậy tốt (bởi vì cha mẹ được fork ở trạng thái đơn giản).

Forkserver

Phương pháp thứ ba, forkserver , được minh họa bên dưới. Lưu ý rằng trẻ em giữ lại một bản sao của trạng thái máy chủ. Trạng thái này được dự định là tương đối đơn giản, nhưng có thể điều chỉnh điều này thông qua API đa quy trình thông qua set_forkserver_preload()phương thức.

Thực hành

Vì vậy, nếu bạn muốn simething được thừa hưởng bởi các quá trình con từ cha mẹ, điều này phải được quy định trong forkserver nhà nước bằng các phương tiện set_forkserver_preload(modules_names), trong đó thiết lập danh sách các tên mô-đun để cố gắng tải trong quá trình forkserver. Tôi đưa ra một ví dụ dưới đây:

# inherited.py
large_obj = {"one": 1, "two": 2, "three": 3}
# main.py
import multiprocessing
import os
from time import sleep

from inherited import large_obj


def worker_func(key: str):
    print(os.getpid(), id(large_obj))
    sleep(1)
    return large_obj[key]


if __name__ == '__main__':
    result = []
    ctx_in_main = multiprocessing.get_context('forkserver')
    ctx_in_main.set_forkserver_preload(['inherited'])
    cores = ctx_in_main.cpu_count()
    with ctx_in_main.Pool(processes=cores) as pool:
        for x in pool.imap(worker_func, ["one", "two", "three"]):
            result.append(x)
    for res in result:
        print(res)

Đầu ra:

# The PIDs are different but the address is always the same
PID=18603, obj id=139913466185024
PID=18604, obj id=139913466185024
PID=18605, obj id=139913466185024

Và nếu chúng ta không sử dụng tính năng tải trước

...
    ctx_in_main = multiprocessing.get_context('forkserver')
    # ctx_in_main.set_forkserver_preload(['inherited']) 
    cores = ctx_in_main.cpu_count()
...
# The PIDs are different, the addresses are different too
# (but sometimes they can coincide)
PID=19046, obj id=140011789067776
PID=19047, obj id=140011789030976
PID=19048, obj id=140011789030912
1 sgyzetrov Aug 17 2020 at 03:00

Vì vậy, sau một cuộc thảo luận đầy cảm hứng với Alex, tôi nghĩ rằng tôi có đủ thông tin để giải quyết câu hỏi của mình: chính xác thì quy trình forkserver sẽ kế thừa điều gì từ quy trình mẹ?

Về cơ bản khi quá trình máy chủ bắt đầu, nó sẽ nhập mô-đun chính của bạn và mọi thứ trước đó if __name__ == '__main__'sẽ được thực thi. Đó là lý do tại sao mã của tôi không hoạt động, bởi vì large_objectkhông có nơi nào được tìm thấy trong serverquy trình và trong tất cả các quy trình công nhân đó tách khỏi serverquy trình .

Giải pháp của Alex hoạt động vì large_objectbây giờ được nhập vào cả quá trình chính và máy chủ, vì vậy mọi nhân viên được phân nhánh từ máy chủ cũng sẽ nhận được large_object. Nếu kết hợp với set_forkserver_preload(modules_names)tất cả các công nhân thậm chí có thể nhận được như nhau large_object từ những gì tôi đã thấy. Lý do sử dụng forkserverđược giải thích rõ ràng trong tài liệu Python và trong blog của Bojan:

Khi chương trình khởi động và chọn phương pháp khởi động máy chủ, quá trình máy chủ sẽ được bắt đầu. Từ đó trở đi, bất cứ khi nào cần một quy trình mới, quy trình mẹ sẽ kết nối với máy chủ và yêu cầu nó phân nhánh một quy trình mới. Quy trình máy chủ fork là một luồng đơn nên có thể an toàn khi sử dụng os.fork (). Không có tài nguyên không cần thiết nào được kế thừa .

Cơ chế forkserver, bao gồm một máy chủ Python riêng biệt với trạng thái tương đối đơn giản và là fork () - ed khi cần một quy trình mới. Phương thức này kết hợp tốc độ của Fork () - ing với độ tin cậy tốt (bởi vì cha mẹ được fork ở trạng thái đơn giản) .

Vì vậy, nó nhiều hơn về mặt an toàn của mối quan tâm ở đây.

Một lưu ý nhỏ là, nếu bạn sử dụng forklàm phương pháp bắt đầu, bạn không cần nhập bất kỳ thứ gì vì tất cả quy trình con đều nhận được bản sao của bộ nhớ quy trình cha (hoặc tham chiếu nếu hệ thống sử dụng COW- copy-on-write, vui lòng sửa cho tôi nếu tôi Sai lầm). Trong trường hợp này sử dụng global large_objectsẽ giúp bạn có được quyền truy cập vào large_objecttrong worker_functrực tiếp.

Đây forkservercó thể không phải là cách tiếp cận phù hợp với tôi vì vấn đề tôi đang phải đối mặt là chi phí bộ nhớ. Tất cả các thao tác đưa tôi large_objectvào vị trí đầu tiên đều tốn bộ nhớ, vì vậy tôi không muốn có bất kỳ tài nguyên không cần thiết nào trong các quy trình công nhân của mình.

Nếu tôi đặt tất cả các tính toán đó trực tiếp vào inherited.pynhư Alex đề xuất, nó sẽ được thực thi hai lần (một lần khi tôi nhập mô-đun vào chính và một lần khi máy chủ nhập nó; thậm chí có thể nhiều hơn khi các quy trình công nhân được sinh ra?), Điều này phù hợp nếu tôi chỉ muốn một quy trình an toàn đơn luồng mà người lao động có thể phân nhánh. Nhưng vì tôi đang cố gắng để người lao động không thừa hưởng các tài nguyên không cần thiết và chỉ nhận được large_object, điều này sẽ không hoạt động. Và đưa những tính toán ở __main__trong inherited.pysẽ không làm việc, hoặc kể từ bây giờ không ai trong số các quá trình sẽ thực hiện chúng, bao gồm cả chính và máy chủ.

Vì vậy, như một kết luận, nếu mục tiêu ở đây là để người lao động thừa hưởng các tài nguyên tối thiểu, thì tốt hơn hết tôi nên chia mã của mình thành 2, làm calculation.pytrước, chọn large_object, thoát trình thông dịch và bắt đầu một mã mới để tải tiếp theo large_object. Sau đó, tôi chỉ có thể phát điên với một trong hai forkhoặc forkserver.