multiprocessing dengan python - apa yang diwarisi oleh proses forkserver dari proses induk?
Saya mencoba menggunakan forkserver
dan saya temui NameError: name 'xxx' is not defined
dalam proses pekerja.
Saya menggunakan Python 3.6.4, tetapi dokumentasinya harus sama, dari https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods dikatakan bahwa:
Proses server garpu adalah utas tunggal sehingga aman untuk menggunakan os.fork (). Tidak ada sumber daya yang tidak perlu diwariskan.
Juga, dikatakan:
Lebih baik mewarisi dari pada acar / tidak diikat
Saat menggunakan metode spawn atau forkserver start, banyak jenis dari multiprocessing harus dapat dipilih sehingga proses anak dapat menggunakannya . Namun, orang biasanya harus menghindari pengiriman objek bersama ke proses lain menggunakan pipa atau antrian. Sebaliknya Anda harus mengatur program sehingga proses yang membutuhkan akses ke sumber daya bersama yang dibuat di tempat lain dapat mewarisinya dari proses leluhur.
Jadi ternyata objek kunci yang perlu dikerjakan oleh proses pekerja saya tidak diwarisi oleh proses server dan kemudian diteruskan ke pekerja, mengapa itu terjadi? Saya bertanya-tanya apa sebenarnya yang diwarisi oleh proses forkserver dari proses induk?
Inilah tampilan kode saya:
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)
Terima kasih!
Terbaik,
Jawaban
Teori
Di bawah ini adalah kutipan dari blog Bojan Nikolic
Versi Python modern (di Linux) menyediakan tiga cara untuk memulai proses terpisah:
Fork () -ing proses induk dan melanjutkan dengan gambar proses yang sama di induk dan anak. Metode ini cepat, tetapi berpotensi tidak dapat diandalkan jika status induknya rumit
Memunculkan proses anak, yaitu fork () - ing dan kemudian execv untuk mengganti image proses dengan proses Python baru. Metode ini dapat diandalkan tetapi lambat, karena gambar proses dimuat ulang lagi.
Mekanisme forkserver , yang terdiri dari server Python terpisah dengan yang memiliki status yang relatif sederhana dan yang bercabang () - ed ketika diperlukan proses baru. Metode ini menggabungkan kecepatan Fork () - ing dengan keandalan yang baik (karena induk yang di-fork dalam keadaan sederhana).
Forkserver
Metode ketiga, forkserver , diilustrasikan di bawah ini. Perhatikan bahwa anak-anak menyimpan salinan status forkserver. Status ini dimaksudkan agar relatif sederhana, tetapi dimungkinkan untuk menyesuaikan ini melalui multiproses API melalui
set_forkserver_preload()
metode.
Praktek
Jadi, jika Anda ingin simething diwarisi oleh proses anak dari induk, ini harus ditentukan dalam status forkserver dengan cara set_forkserver_preload(modules_names)
, yang mengatur daftar nama modul untuk mencoba memuat dalam proses forkserver. Saya berikan contoh di bawah ini:
# 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)
Keluaran:
# 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
Dan jika kami tidak menggunakan preloading
...
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
Jadi setelah diskusi yang menginspirasi dengan Alex, saya pikir saya memiliki info yang cukup untuk menjawab pertanyaan saya: apa sebenarnya yang diwarisi oleh proses forkserver dari proses induk?
Pada dasarnya ketika proses server dimulai, itu akan mengimpor modul utama Anda dan semua yang sebelumnya if __name__ == '__main__'
akan dijalankan. Itulah mengapa kode saya tidak berfungsi, karena large_object
tidak dapat ditemukan di mana pun dalam server
proses dan dalam semua proses pekerja yang bercabang dari server
proses tersebut .
Solusi Alex berfungsi karena large_object
sekarang diimpor ke proses utama dan server sehingga setiap pekerja yang bercabang dari server juga akan mendapatkannya large_object
. Jika digabungkan dengan set_forkserver_preload(modules_names)
semua pekerja bahkan mungkin akan mendapatkan yang sama large_object
dari apa yang saya lihat. Alasan penggunaan forkserver
dijelaskan secara eksplisit dalam dokumentasi Python dan di blog Bojan:
Ketika program dimulai dan memilih metode mulai forkserver, proses server dimulai. Sejak saat itu, setiap kali proses baru diperlukan, proses induk terhubung ke server dan memintanya untuk melakukan proses baru. Proses server garpu adalah utas tunggal sehingga aman untuk menggunakan os.fork (). Tidak ada sumber daya yang tidak perlu diwariskan .
Mekanisme forkserver, yang terdiri dari server Python terpisah dengan yang memiliki status yang relatif sederhana dan yang bercabang () - ed ketika diperlukan proses baru. Metode ini menggabungkan kecepatan Fork () - ing dengan keandalan yang baik (karena induk yang di-fork dalam keadaan sederhana) .
Jadi lebih pada sisi aman menjadi perhatian di sini.
Di samping catatan, jika Anda menggunakan fork
sebagai metode awal, Anda tidak perlu mengimpor apa pun karena semua proses anak mendapat salinan memori proses orang tua (atau referensi jika sistem menggunakan COW- copy-on-write
, tolong perbaiki saya jika saya salah). Dalam hal ini menggunakan global large_object
akan membuat Anda akses ke large_object
dalam worker_func
secara langsung.
The forkserver
kekuatan tidak menjadi pendekatan yang cocok untuk saya karena masalah saya hadapi adalah overhead memori. Semua operasi yang membuat saya large_object
memakan banyak memori, jadi saya tidak ingin ada sumber daya yang tidak perlu dalam proses pekerja saya.
Jika saya memasukkan semua kalkulasi itu langsung ke dalam inherited.py
seperti yang disarankan Alex, itu akan dieksekusi dua kali (sekali ketika saya mengimpor modul di main dan sekali ketika server mengimpornya; bahkan mungkin lebih ketika proses pekerja lahir?), Ini cocok jika saya hanya menginginkan proses aman single-threaded yang dapat digunakan oleh pekerja. Tetapi karena saya mencoba membuat pekerja tidak mewarisi sumber daya yang tidak perlu dan hanya mendapatkan large_object
, ini tidak akan berhasil. Dan menempatkan perhitungan tersebut di __main__
dalam inherited.py
tidak akan bekerja baik karena sekarang tidak ada proses akan mengeksekusi mereka, termasuk utama dan server.
Jadi, sebagai kesimpulan, jika tujuannya di sini adalah untuk membuat pekerja mewarisi sumber daya minimal, saya lebih baik memecah kode saya menjadi 2, lakukan calculation.py
pertama, buat acar large_object
, keluar dari penerjemah, dan mulai yang baru untuk memuat acar large_object
. Lalu aku bisa jadi gila dengan fork
atau forkserver
.