Concurrent.futures vs Multiprocessing dengan Python 3

148

Python 3.2 memperkenalkan Concurrent Futures , yang tampaknya merupakan kombinasi lanjutan dari modul threading dan multiprocessing yang lebih tua .

Apa kelebihan dan kekurangan menggunakan ini untuk tugas-tugas yang terikat CPU atas modul multiprosesor yang lebih lama?

Artikel ini menunjukkan bahwa mereka lebih mudah untuk dikerjakan - apakah itu masalahnya?

GIS-Jonathan
sumber

Jawaban:

145

Saya tidak akan menyebut concurrent.futureslebih "maju" - ini adalah antarmuka yang lebih sederhana yang bekerja sangat mirip terlepas dari apakah Anda menggunakan beberapa utas atau beberapa proses sebagai gimmick paralelisasi yang mendasarinya.

Jadi, seperti hampir semua contoh "antarmuka yang lebih sederhana", banyak trade-off yang sama terlibat: ia memiliki kurva belajar yang lebih dangkal, sebagian besar hanya karena ada begitu banyak yang tersedia untuk dipelajari; tetapi, karena ia menawarkan lebih sedikit opsi, pada akhirnya ia mungkin membuat Anda frustrasi dengan cara yang tidak dimiliki oleh antarmuka yang lebih kaya.

Sejauh tugas-tugas yang terikat CPU, itu terlalu kurang ditentukan untuk dikatakan lebih bermakna. Untuk tugas-tugas yang terikat CPU di bawah CPython, Anda membutuhkan beberapa proses daripada beberapa utas untuk memiliki peluang mendapatkan speedup. Tetapi seberapa banyak (jika ada) dari speedup yang Anda dapatkan tergantung pada detail perangkat keras Anda, OS Anda, dan terutama pada seberapa banyak komunikasi antar-proses yang dibutuhkan tugas spesifik Anda. Di bawah penutup, semua gimmicks paralelisasi antar proses bergantung pada primitif OS yang sama - API tingkat tinggi yang Anda gunakan untuk mendapatkannya bukanlah faktor utama dalam kecepatan bottom-line.

Edit: contoh

Berikut ini kode terakhir yang ditampilkan di artikel yang Anda referensikan, tetapi saya menambahkan pernyataan impor yang diperlukan untuk membuatnya berfungsi:

from concurrent.futures import ProcessPoolExecutor
def pool_factorizer_map(nums, nprocs):
    # Let the executor divide the work among processes by using 'map'.
    with ProcessPoolExecutor(max_workers=nprocs) as executor:
        return {num:factors for num, factors in
                                zip(nums,
                                    executor.map(factorize_naive, nums))}

Inilah hal yang persis sama menggunakan multiprocessingsebagai gantinya:

import multiprocessing as mp
def mp_factorizer_map(nums, nprocs):
    with mp.Pool(nprocs) as pool:
        return {num:factors for num, factors in
                                zip(nums,
                                    pool.map(factorize_naive, nums))}

Perhatikan bahwa kemampuan untuk menggunakan multiprocessing.Poolobjek sebagai manajer konteks ditambahkan dalam Python 3.3.

Yang mana yang lebih mudah untuk dikerjakan? LOL ;-) Mereka pada dasarnya identik.

Salah satu perbedaan adalah bahwa Pooldukungan begitu banyak cara yang berbeda dalam melakukan hal-hal yang Anda mungkin tidak menyadari betapa mudahnya dapat menjadi sampai Anda sudah naik cukup jalan sampai kurva belajar.

Sekali lagi, semua cara yang berbeda itu merupakan kekuatan sekaligus kelemahan. Mereka adalah kekuatan karena fleksibilitas mungkin diperlukan dalam beberapa situasi. Mereka adalah kelemahan karena "lebih disukai hanya satu cara yang jelas untuk melakukannya". Sebuah proyek yang bertahan secara eksklusif (jika mungkin) concurrent.futuresmungkin akan lebih mudah untuk dipertahankan dalam jangka panjang, karena kurangnya kebaruan yang tidak disengaja dalam bagaimana API minimalnya dapat digunakan.

Tim Peters
sumber
20
"Anda membutuhkan banyak proses daripada beberapa utas untuk memiliki peluang mendapatkan percepatan" terlalu keras. Jika kecepatan penting; kode mungkin sudah menggunakan pustaka C dan oleh karena itu ia dapat melepaskan GIL misalnya, regex, lxml, numpy.
jfs
4
@ JSFSebastian, terima kasih telah menambahkan itu - mungkin saya seharusnya mengatakan "di bawah CPython murni ", tapi saya khawatir tidak ada cara singkat untuk menjelaskan kebenaran di sini tanpa mendiskusikan GIL.
Tim Peters
2
Dan perlu disebutkan bahwa utas mungkin sangat berguna dan cukup ketika beroperasi dengan IO lama.
kotrfa
9
@TimPeters Dalam beberapa hal ProcessPoolExecutorsebenarnya memiliki lebih banyak pilihan daripada Poolkarena ProcessPoolExecutor.submitpengembalian Futurekasus yang memungkinkan pembatalan ( cancel), memeriksa yang terkecuali dibesarkan ( exception), dan dinamis menambahkan callback yang akan dipanggil setelah selesai ( add_done_callback). Tidak satu pun dari fitur ini tersedia dengan AsyncResultinstance yang dikembalikan oleh Pool.apply_async. Dalam cara lain Poolmemiliki lebih banyak pilihan karena initializer/ initargs, maxtasksperchilddan contextdi Pool.__init__, dan metode yang lebih terpapar oleh Poolcontoh.
maks
2
@ Max, tentu saja, tetapi perhatikan bahwa pertanyaannya bukan tentang Pool, itu tentang modul. Pooladalah bagian kecil dari apa yang ada di multiprocessingdalamnya, dan begitu jauh di dalam dokumen, butuh beberapa saat bagi orang untuk menyadari keberadaannya multiprocessing. Jawaban khusus ini difokuskan Poolkarena hanya itulah artikel yang ditautkan oleh OP, dan itu cf"jauh lebih mudah untuk dikerjakan" sama sekali tidak benar tentang apa yang artikel tersebut bahas. Di luar itu, cf's as_completed()juga bisa sangat berguna.
Tim Peters