Python 3'te Concurrent.futures ve Multiprocessing karşılaştırması


Yanıtlar:


152

Ben concurrent.futuresdaha "gelişmiş" demezdim - temel paralelleştirme hilesi olarak birden çok iş parçacığı veya birden çok işlem kullansanız da hemen hemen aynı şekilde çalışan daha basit bir arayüz.

Yani, "daha basit bir arayüz" hemen hemen tüm örneklerde olduğu gibi, hemen hemen aynı dengelemeler katılmaktadırlar: çok daha az müsait var diye o büyük ölçüde, daha sığ bir öğrenme eğrisi vardır etmek öğrenilebilir; ancak, daha az seçenek sunduğu için, daha zengin arayüzlerin olmayacağı şekillerde sonunda sizi hayal kırıklığına uğratabilir.

CPU'ya bağlı görevler söz konusu olduğunda, bu, çok anlamlı bir şey söylemek için çok az belirtilmiş. CPython altındaki CPU'ya bağlı görevler için, hızlanma şansına sahip olmak için birden çok iş parçacığı yerine birden çok işleme ihtiyacınız vardır. Ancak ne kadar hızlanma (varsa) alacağınız, donanımınızın ayrıntılarına, işletim sisteminize ve özellikle belirli görevlerinizin ne kadar süreçler arası iletişim gerektirdiğine bağlıdır. Kapakların altında, tüm süreçler arası paralelleştirme hileleri aynı işletim sistemi temellerine dayanır - bunlara ulaşmak için kullandığınız yüksek seviyeli API, en alt satır hızındaki birincil faktör değildir.

Düzenleme: örnek

İşte başvurduğunuz makalede gösterilen son kod, ancak çalışması için gerekli olan bir import ifadesi ekliyorum:

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))}

İşte tam olarak aynı şeyi kullanarak multiprocessing:

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))}

multiprocessing.PoolNesneleri bağlam yöneticileri olarak kullanma yeteneğinin Python 3.3'e eklendiğini unutmayın.

Hangisiyle çalışmanın daha kolay olduğuna gelince, bunlar aslında aynıdır.

Farklardan biri olan Pooldesteklerinin bunu ne kadar kolay fark olmayabilir şeyler yapmanın çok farklı şekillerde böylece edebilirsiniz sen uzunca bir yol öğrenme eğrisi tırmandı ettik kadar olacak.

Yine, tüm bu farklı yollar hem güç hem de zayıflıktır. Bir güçtürler çünkü bazı durumlarda esneklik gerekebilir. "Tercihen bunu yapmanın tek açık yolu" nedeniyle bir zayıflıktır. concurrent.futuresMinimal API'sinin nasıl kullanılabileceği konusunda gereksiz yenilik olmaması nedeniyle, yalnızca (mümkünse) bağlı bir projenin uzun vadede sürdürülmesi muhtemelen daha kolay olacaktır.


20
"Hızlanma şansına sahip olmak için birden çok iş parçacığı yerine birden çok işleme ihtiyacınız var" çok zordur. Hız önemliyse; kod zaten bir C kitaplığı kullanıyor olabilir ve bu nedenle GIL'i serbest bırakabilir, ör. regex, lxml, numpy.
jfs

4
@JFSebastian, bunu eklediğin için teşekkürler - belki de " saf CPython altında" demeliydim, ama korkarım burada GIL'i tartışmadan gerçeği açıklamanın kısa bir yolu yok.
Tim Peters

2
Ve uzun IO ile işlem yaparken iş parçacığının özellikle yararlı ve yeterli olabileceğinden bahsetmeye değer.
kotrfa

11
@TimPeters Bazı yönlerden ProcessPoolExecutoraslında daha fazla seçeneğe sahiptir Poolçünkü cancellation ( ) 'a izin veren örnekler ProcessPoolExecutor.submitdöndürür , hangi istisnanın ortaya Futureçıktığını cancelkontrol eder ( ) ve tamamlandığında çağrılacak bir geri aramayı dinamik olarak ekler ( ). Bu özelliklerin hiçbiri, tarafından döndürülen örneklerle kullanılamaz . Diğer yönden nedeniyle daha fazla seçeneğe sahip / , ve de , ve tarafından maruz daha yöntemleri örneği. exceptionadd_done_callbackAsyncResultPool.apply_asyncPoolinitializerinitargsmaxtasksperchildcontextPool.__init__Pool
en fazla

2
@max, elbette, ama sorunun Poolmodüllerle ilgili olmadığını unutmayın . Poolbu, içinde olanın küçük bir parçasıdır multiprocessingve belgelerde o kadar aşağıdadır ki, insanların içinde var olduğunu bile fark etmesi biraz zaman alır multiprocessing. Bu özel cevap odaklandı Poolçünkü OP'nin kullandığı cftüm makale buydu ve bu "çalışmak çok daha kolay", makalenin tartıştığı şey hakkında doğru değil. Bunun ötesinde cfs as_completed()de çok kullanışlı olabilir.
Tim Peters

1

Diğer yanıtların ayrıntılı farklılık listesine ek olarak, kişisel olarak, çoklu işlemde meydana gelebilecek sabit olmayan (2020-10-27 itibariyle) belirsiz bir asılma ile karşılaştım.Çalışanlardan biri belirli şekillerde çöktüğünde havuz . (Benim durumumda, bir cython uzantısından bir istisna, ancak diğerleri bunun bir işçi bir SIGTERM vb. Aldığında olabileceğini söylese de) ProcessPoolExecutor belgelerine göre , python 3.3'ten beri bu konuda sağlamdır.


1

Muhtemelen paralel işlemeye ihtiyaç duyduğunuzda çoğu zaman, ya modüldeki ProcessPoolExecutorsınıfın ya da concurrent.futuresmodüldeki Poolsınıfın multiprocessingeşdeğer kolaylıklar sağlayacağını ve kişisel tercih meselesine dönüştüğünü göreceksiniz . Ancak her biri, belirli işlemleri daha kolay hale getiren bazı olanaklar sunar. Sadece bir çift göstereceğimi düşündüm:

Bir grup görevi gönderirken, bazen görev sonuçlarını (yani değerleri döndürür) kullanılabilir olur olmaz almak istersiniz. Her iki tesis de, gönderilen bir görevin sonucunun geri arama mekanizmaları aracılığıyla kullanılabilir olduğuna dair bildirim sağlar:

Çoklu işlemeyi kullanma.Havuz:

import multiprocessing as mp

def worker_process(i):
    return i * i # square the argument

def process_result(return_value):
    print(return_value)

def main():
    pool = mp.Pool()
    for i in range(10):
        pool.apply_async(worker_process, args=(i,), callback=process_result)
    pool.close()
    pool.join()

if __name__ == '__main__':
    main()

Aynısı, garip bir şekilde de olsa, aşağıdakilerle bir geri arama kullanılarak yapılabilir concurrent.futures:

import concurrent.futures

def worker_process(i):
    return i * i # square the argument

def process_result(future):
    print(future.result())

def main():
    executor = concurrent.futures.ProcessPoolExecutor()
    futures = [executor.submit(worker_process, i) for i in range(10)]
    for future in futures:
        future.add_done_callback(process_result)
    executor.shutdown()

if __name__ == '__main__':
    main()

Burada her görev ayrı ayrı gönderilir ve bunun için bir Futureörnek döndürülür. Daha sonra geri arama Future,. Son olarak, geri çağırma çağrıldığında, geçirilen bağımsız değişken, Futuretamamlanan görevin örneğidir resultve gerçek dönüş değerini almak için yöntemin çağrılması gerekir. Ancak concurrent.futuresmodül ile, aslında bir geri aramayı kullanmaya gerek yoktur. as_completedYöntemi kullanabilirsiniz :

import concurrent.futures

def worker_process(i):
    return i * i # square the argument

def main():
    with concurrent.futures.ProcessPoolExecutor() as executor:
        futures = [executor.submit(worker_process, i) for i in range(10)]
        for future in concurrent.futures.as_completed(futures):
            print(future.result())

if __name__ == '__main__':
    main()

Ve örnekleri worker_processtutmak için bir sözlük kullanarak , dönüş değerini orijinal geçirilen bağımsız değişkene geri bağlamak kolaydır Future:

import concurrent.futures

def worker_process(i):
    return i * i # square the argument

def main():
    with concurrent.futures.ProcessPoolExecutor() as executor:
        futures = {executor.submit(worker_process, i): i for i in range(10)}
        for future in concurrent.futures.as_completed(futures):
            i = futures[future] # retrieve the value that was squared
            print(i, future.result())

if __name__ == '__main__':
    main()

Ancak aşılmaması için, multiprocessing.Poolgörev sonuçlarının tamamlanırken işlenmesine izin veren bir yöntemi vardır:

import multiprocessing as mp

def worker_process(i):
    return i * i # square the argument

def main():
    cpu_count = mp.cpu_count()
    N = 100
    chunk_size = N // cpu_count
    with mp.Pool() as pool:
        for result in pool.imap_unordered(worker_process, range(N), chunksize=chunk_size):
            print(result)

if __name__ == '__main__':
    main()

Ancak imap_unordered, çalışan işlem dönüş değeriyle birlikte orijinal çağrı bağımsız değişkenlerini döndürmedikçe, bir sonucu gönderilen bir işle kolayca bağlamanın bir yolu yoktur. Öte yandan, sonuçların öngörülebilir bir sırada olacağı bir chunksizeile imap_unorderdve belirtme yeteneği , bu yöntemleri , esasen 1'lik bir yığın boyutu kullanan sınıfın yönteminden daha verimli hale getirebilir .imapsubmitconcurrent.futures.ProcessPoolExector

multiprocessing.PoolSınıf bir yöntemi vardır applysonuç hazır olana kadar havuz ve bloklar bir görev gönderir. Dönüş değeri, işleve aktarılan işçi işlevinin yalnızca dönüş değeridir apply. Örneğin:

import multiprocessing as mp

def worker_process(i):
    return i * i # square the argument

def main():
    with mp.Pool() as pool:
        print(pool.apply(worker_process, args=(6,)))
        print(pool.apply(worker_process, args=(4,)))

if __name__ == '__main__':
    main()

concurrent.futures.ThreadPoolExecutorSınıf böyle bir eşdeğeri yer alır. Döndürülen örneğe bir submitve ardından bir çağrı yapmalısınız . Bunu yapmak zor değil, ancak yöntem, engelleyici bir görev gönderiminin uygun olduğu kullanım durumu için daha uygundur. Böyle bir durum, iş parçacığı için çağrıları işlediğiniz zamandır çünkü iş parçacıklarında yapılan işlerin çoğu, CPU'ya çok bağlı olan bir işlev haricinde yoğun bir şekilde I / O'dur. Evreleri oluşturan ana program önce bir örnek yaratır ve bunu tüm evrelere argüman olarak iletir. İş parçacığının yoğun şekilde CPU'ya bağlı işlevi çağırması gerektiğinde, şimdi işlevi kullanarakresultFuturepool.applymultiprocessing.Poolpool.apply yöntem böylece kodu başka bir işlemde çalıştırır ve diğer iş parçacıklarının çalışmasına izin vermek için mevcut işlemi serbest bırakır.

concurrent.futuresModülün iki sınıfa ProcessPoolExecutorve ThreadPoolExecutoraynı arayüzlere sahip olması büyük bir anlaşma yapıldı . Bu güzel bir özellik. Ancak multiprocessingmodül, aşağıdakilerle ThreadPoolaynı arayüze sahip belgelenmemiş bir sınıfa sahiptir Pool:

>>> from multiprocessing.pool import Pool
>>> from multiprocessing.pool import ThreadPool
>>> dir(Pool)
['Process', '__class__', '__del__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_check_running', '_get_sentinels', '_get_tasks', '_get_worker_sentinels', '_guarded_task_generation', '_handle_results', '_handle_tasks', '_handle_workers', '_help_stuff_finish', '_join_exited_workers', '_maintain_pool', '_map_async', '_repopulate_pool', '_repopulate_pool_static', '_setup_queues', '_terminate_pool', '_wait_for_updates', '_wrap_exception', 'apply', 'apply_async', 'close', 'imap', 'imap_unordered', 'join', 'map', 'map_async', 'starmap', 'starmap_async', 'terminate']
>>> dir(ThreadPool)
['Process', '__class__', '__del__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_check_running', '_get_sentinels', '_get_tasks', '_get_worker_sentinels', '_guarded_task_generation', '_handle_results', '_handle_tasks', '_handle_workers', '_help_stuff_finish', '_join_exited_workers', '_maintain_pool', '_map_async', '_repopulate_pool', '_repopulate_pool_static', '_setup_queues', '_terminate_pool', '_wait_for_updates', '_wrap_exception', 'apply', 'apply_async', 'close', 'imap', 'imap_unordered', 'join', 'map', 'map_async', 'starmap', 'starmap_async', 'terminate']
>>>

ProcessPoolExecutor.submitBir Futureörnek döndüren veya Pool.apply_asyncbir AsyncResultörnek döndüren ve sonucu almak için bir zaman aşımı değeri belirleyen görevler ile görev gönderebilirsiniz :

from concurrent.futures import ProcessPoolExecutor, TimeoutError
from time import sleep


def worker_1():
    while True:
        print('hanging')
        sleep(1)


def main():
    with ProcessPoolExecutor(1) as pool:
        future = pool.submit(worker_1)
        try:
            future.result(3) # kill task after 3 seconds?
        except TimeoutError:
            print('timeout')

if __name__ == '__main__':
    main()
    print("return from main()")

Baskılar:

hanging
hanging
hanging
timeout
hanging
hanging
hanging
hanging
hanging
hanging
hanging
etc.

Gönderilen görev bu süre içinde tamamlanmadığından , arama sırasındaki ana işlem 3 saniye sonra future.result(3)bir TimeoutErroristisna alır. Ancak görev çalışmaya devam ediyor, süreci bağlar ve with ProcessPoolExecutor(1) as pool:blok asla çıkmaz ve bu nedenle program sona ermez.

from multiprocessing import Pool, TimeoutError
from time import sleep


def worker_1():
    while True:
        print('hanging')
        sleep(1)

def main():
    with Pool(1) as pool:
        result = pool.apply_async(worker_1, args=())
        try:
            result.get(3) # kill task after 3 seconds?
        except TimeoutError:
            print('timeout')


if __name__ == '__main__':
    main()
    print("return from main()")

Baskılar:

hanging
hanging
hanging
timeout
return from main()

Ancak bu sefer, zaman aşımına uğrayan görev hala çalışmaya devam ediyor ve işlemi yazıyor olsa bile, withbloğun çıkması engellenmez ve bu nedenle program normal şekilde sona erer. Avantaj gidecek gibi görünüyor multiprocessing.Pool.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.