Diş açma ve çok işlemli modüller arasındaki farklar nelerdir?


141

Belirli işlemleri paralel olarak çalıştırmak ve kodumu hızlandırmak için Python'daki threadingve multiprocessingmodüllerinin nasıl kullanılacağını öğreniyorum .

Bir threading.Thread()nesne ile bir nesne arasındaki farkın ne olduğunu anlamak için bunu zor buluyorum (belki de bununla ilgili teorik bir arka planım yok) multiprocessing.Process().

Ayrıca, bir iş kuyruğunu nasıl somutlaştıracağımı ve sadece 4'ünün (örneğin) paralel çalışmasını nasıl sağladığımı tam olarak bilmiyorum, diğeri ise yürütülmeden önce kaynakların serbest kalmasını bekliyor.

Belgelerdeki örnekleri net buluyorum, ancak çok ayrıntılı değil; bir şeyleri biraz karmaşıklaştırmaya çalıştığımda, çok garip hatalar alıyorum (turşu olamayan bir yöntem gibi).

Peki threadingve multiprocessingmodüllerini ne zaman kullanmalıyım ?

Beni bu iki modülün arkasındaki kavramları ve bunları karmaşık görevler için nasıl doğru bir şekilde kullanacağınızı açıklayan bazı kaynaklara bağlayabilir misiniz?


Dahası var, Threadmodül de var ( _threadpython 3.x'de denir ). Dürüst olmak gerekirse, farkları kendim hiç anlamadım ...
Dunno

3
@Dunno: Thread/ _threadBelgelerin açıkça belirttiği gibi, "düşük düzeyli ilkeller". Özel senkronizasyon nesneleri oluşturmak, bir iş parçacığı ağacının birleşim sırasını vb. Kontrol etmek için kullanabilirsiniz threading.
abarnert

Yanıtlar:


260

Ne Giulio Franco diyor çoklu işlem vs çoklu kullanım için geçerlidir genelde .

Ancak, Python * ek bir sorun vardır: Aynı işlemdeki iki iş parçacığının aynı anda Python kodunu çalıştırmasını engelleyen bir Global Tercüman Kilidi vardır. Bu, 8 çekirdeğiniz varsa ve kodunuzu 8 iş parçacığı kullanacak şekilde değiştirirseniz,% 800 CPU kullanamaz ve 8 kat daha hızlı çalışamaz; aynı% 100 CPU'yu kullanır ve aynı hızda çalışır. (Gerçekte, biraz daha yavaş çalışacaktır, çünkü paylaşılan verileriniz olmasa bile, iş parçacığından ekstra ek yük var, ancak şimdilik bunu görmezden gelin.)

Bunun istisnaları var. Kodunuzun ağır hesaplaması aslında Python'da olmasa da, numpy uygulaması gibi uygun GIL işlemeyi gerçekleştiren özel C koduna sahip bazı kütüphanelerde, diş açmadan beklenen performans avantajını elde edersiniz. Eğer ağır hesaplama çalıştırdığınız ve beklediğiniz bazı alt süreçler tarafından yapılıyorsa aynı şey geçerlidir.

Daha da önemlisi, bunun önemli olmadığı durumlar vardır. Örneğin, bir ağ sunucusu zamanının çoğunu paketleri okuma ağ üzerinden geçirir ve bir GUI uygulaması zamanının çoğunu kullanıcı olaylarını beklerken geçirir. Bir ağ sunucusunda veya GUI uygulamasında iş parçacığı kullanmanın bir nedeni, ana iş parçacığının hizmet ağ paketlerine veya GUI olaylarına devam etmesini durdurmadan uzun süren "arka plan görevleri" yapmanıza izin vermektir. Ve bu Python iş parçacıkları ile gayet iyi çalışıyor. (Teknik açıdan bakıldığında, Python iş parçacığı, size çekirdek paralellik vermese de size eşzamanlılık kazandırdığı anlamına gelir.)

Ancak saf Python'da CPU'ya bağlı bir program yazıyorsanız, daha fazla iş parçacığı kullanmak genellikle yardımcı olmaz.

Ayrı işlemlerin GIL ile ilgili herhangi bir sorunu yoktur, çünkü her işlemin kendi ayrı GIL'si vardır. Tabii ki, diğer dillerde olduğu gibi, iş parçacıkları ve süreçler arasında hala aynı dengeye sahipsiniz - süreçler arasında veri paylaşmak iş parçacıkları arasında olduğundan daha zor ve daha pahalıdır, çok sayıda işlem yürütmek veya oluşturmak ve yok etmek pahalı olabilir Ancak GIL, örneğin C veya Java için doğru olmayan bir şekilde, süreçler arasındaki dengeye dayanır. Böylece, Python'da çok işlemciliği C veya Java'da olduğundan çok daha sık kullandığınızı göreceksiniz.


Bu arada, Python'un "piller dahil" felsefesi bazı iyi haberler getiriyor: Tek satırlık bir değişiklikle iş parçacıkları ve işlemler arasında ileri geri değiştirilebilen kod yazmak çok kolay.

Kodunuzu, girdi ve çıktı dışındaki diğer işlerle (veya ana programla) hiçbir şey paylaşmayan bağımsız "işler" olarak tasarlarsanız, kodunuzu aşağıdaki concurrent.futuresgibi bir iş parçacığı havuzuna yazmak için kitaplığı kullanabilirsiniz :

with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
    executor.submit(job, argument)
    executor.map(some_function, collection_of_independent_things)
    # ...

Hatta bu işlerin sonuçlarını alabilir ve başka işlere aktarabilir, idam sırasına veya tamamlanma sırasına, vb. Bekleyebilirsiniz; Futureayrıntılar için nesneler bölümünü okuyun .

Şimdi, programınız sürekli olarak% 100 CPU kullanıyorsa ve daha fazla iş parçacığı eklemek sadece yavaşlatıyorsa, GIL sorunuyla karşılaşıyorsunuz, bu yüzden süreçlere geçmeniz gerekiyor. Tek yapmanız gereken ilk satırı değiştirmek:

with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:

Tek gerçek uyarı, işlerinizin argümanlarının ve dönüş değerlerinin kullanılabilir çapraz işlem olması için seçilebilir (ve turşu için çok fazla zaman veya bellek almaması) gerektiğidir. Genellikle bu bir problem değildir, ama bazen de olabilir.


Peki ya işleriniz kendi kendine yetmezse? Kodunuzu, iletileri birinden diğerine geçiren işler açısından tasarlayabiliyorsanız, yine de oldukça kolaydır. Havuzları kullanmak yerine threading.Threadya da onlara multiprocessing.Processgüvenmek zorunda kalabilirsiniz . Ve açıkça queue.Queueveya multiprocessing.Queuenesneler oluşturmanız gerekecek . (Çok sayıda başka seçenek vardır - borular, soketler, sürülere sahip dosyalar… ama asıl nokta, bir Yöneticinin otomatik büyüsü yetersizse manuel olarak bir şeyler yapmanız gerekir .)

Peki ya mesaj geçişine bile güvenemezseniz? Aynı yapıyı değiştirmek ve birbirlerinin değişikliklerini görmek için iki işe ihtiyacınız varsa ne olur? Bu durumda, el ile eşitleme (kilitler, semaforlar, koşullar vb.) Yapmanız ve işlemleri kullanmak istiyorsanız, önyükleme yapmak için açık paylaşılan bellek nesneleri kullanmanız gerekir. Bu, çoklu iş parçacığının (veya çoklu işlemin) güçleştiği zamandır. Bundan kaçınabiliyorsanız, harika; Eğer yapamazsanız, bir kişinin SO cevabına koyabileceğinden daha fazlasını okumalısınız.


Bir yorumdan, Python'daki iş parçacıkları ve işlemler arasında neyin farklı olduğunu bilmek istediniz. Gerçekten, Giulio Franco'nun cevabını ve benimkini ve tüm bağlantılarımızı okursanız, bu her şeyi kapsamalıdır… ama bir özet kesinlikle yararlı olacaktır, işte burada:

  1. Konular varsayılan olarak veri paylaşır; süreçler değil.
  2. (1) 'in bir sonucu olarak, süreçler arasında veri göndermek genellikle veriyi temizlemeyi ve seçimini kaldırmayı gerektirir. **
  3. (1) 'in bir başka sonucu olarak, doğrudan süreçler arasında veri paylaşımı genellikle verilerin Değer, Dizi ve ctypestürler gibi düşük düzeyli biçimlere yerleştirilmesini gerektirir .
  4. İşlemler GIL'e tabi değildir.
  5. Bazı platformlarda (özellikle Windows), süreçlerin oluşturulması ve yok edilmesi çok daha pahalıdır.
  6. Süreçlerde, bazıları farklı platformlarda farklı olan bazı ekstra kısıtlamalar vardır. Ayrıntılar için Programlama yönergelerine bakın.
  7. threadingModül bazı özellikleri yoktur multiprocessingmodülü. ( multiprocessing.dummyEksik API'nin çoğunu iş parçacıklarının üstüne almak için kullanabilirsiniz veya concurrent.futuresbunun gibi üst düzey modüller kullanabilirsiniz ve endişelenmeyin.)

* Aslında bu sorunu yaşayan Python değil, CPython, o dilin "standart" uygulamasıdır. Jython gibi diğer bazı uygulamalarda GIL yoktur.

** Çatal başlatma yöntemini, Windows olmayan platformların çoğunda yapabileceğiniz çoklu işlem için kullanıyorsanız, her alt işlem, ebeveyn başlatıldığında çocuk başlatıldığında sahip olduğu kaynakları alır; bu, verileri çocuklara iletmenin başka bir yolu olabilir.


teşekkürler, ama her şeyi anladığımdan emin değilim. Her neyse, öğrenme amacıyla biraz yapmaya çalışıyorum ve biraz iş parçacığı naif kullanımı ile kodumun hızını yarıya düşürdüm (aynı anda 1000'den fazla iş parçacığından başlayarak, her bir harici uygulamayı çağırıyor .. bu doyuruyor) cpu, ancak hızda x2 artış var). Akıllıca iplik yönetmek gerçekten benim kod hızını artırabilir düşünüyorum ..
lucacerone

3
@LucaCerone: Ah, kodunuz zamanının çoğunu harici programlarda beklemeye harcıyorsa, evet, iş parçacığından faydalanacaktır. İyi bir nokta. Bunu açıklamak için cevabı düzenleyeyim.
abarnert

2
@LucaCerone: Bu arada, hangi parçaları anlamıyorsun? Başladığınız bilgi düzeyini bilmeden, iyi bir cevap yazmak zordur… ama bazı geri bildirimlerle, belki size ve gelecekteki okuyuculara yardımcı olacak bir şey bulabiliriz.
abarnert

3
@LucaCerone Burada çoklu işleme için PEP'i okumalısınız . Çok işlemciliğe karşı iş parçacıklarının zamanlamaları ve örneklerini verir.
mr2ert

1
@LucaCerone: Yöntemin bağlı olduğu nesnenin karmaşık bir durumu yoksa, dekapaj sorunu için en basit çözüm, nesneyi oluşturan ve yöntemini çağıran aptal bir sarma işlevi yazmaktır. O Eğer yok (; oldukça kolay olan karmaşık durumu var, o zaman muhtemelen picklable yapmak gerekir pickledokümanlar açıklamak) ve ardından en kötü aptal sarıcı def wrapper(obj, *args): return obj.wrapper(*args).
abarnert

32

Tek bir işlemde birden çok iş parçacığı bulunabilir. Aynı işleme ait olan iş parçacıkları aynı bellek alanını paylaşır (aynı değişkenlerden okuyabilir ve bu değişkenlere yazabilir ve birbirleriyle karışabilir). Aksine, farklı süreçler farklı bellek alanlarında yaşar ve her birinin kendi değişkenleri vardır. İletişim kurmak için süreçlerin diğer kanalları (dosyalar, borular veya soketler) kullanması gerekir.

Bir hesaplamayı paralel hale getirmek istiyorsanız, muhtemelen çok iş parçacığına ihtiyacınız olacaktır, çünkü büyük olasılıkla iş parçacıklarının aynı bellekte işbirliği yapmasını istiyorsunuz.

Performanstan bahsetmişken, iş parçacıklarının oluşturulması ve yönetilmesi süreçlerden daha hızlıdır (çünkü işletim sisteminin tamamen yeni bir sanal bellek alanı ayırması gerekmez) ve iş parçacıkları arası iletişim genellikle süreçler arası iletişimden daha hızlıdır. Ancak iş parçacıklarının programlanması daha zordur. İş parçacıkları birbirini etkileyebilir ve birbirlerinin belleğine yazabilir, ancak bunun nasıl gerçekleştiği her zaman açık değildir (çeşitli faktörler, esas olarak talimat yeniden sıralaması ve bellek önbelleğe alma nedeniyle) ve bu nedenle erişimi kontrol etmek için senkronizasyon ilkelerine ihtiyacınız olacak değişkenlerinize.


12
Bu, GIL hakkında yanıltıcı yapan bazı önemli bilgileri eksik.
abarnert

1
@ mr2ert: Evet, özet olarak bu çok önemli bilgiler. :) Ama bundan biraz daha karmaşık, bu yüzden ayrı bir cevap yazdım.
abarnert

2
@Abarnert'ın doğru olduğunu söyleyerek yorum yaptığımı sanıyordum ve burada cevaplarken GIL'i unuttum. Yani bu cevap yanlış, oy kullanmamalısınız.
Giulio Franco

6
Bu cevabı indirdim, çünkü hala Python threadingve arasındaki farkın ne olduğunu hiç cevaplamıyor multiprocessing.
Antti Haapala

Her süreç için bir GIL olduğunu okudum. Ancak tüm süreçler aynı python yorumlayıcı kullanıyor mu veya her iş parçacığı için ayrı yorumlayıcı var mı?
değişkeni

3

Bu bağlantının sorunuzu zarif bir şekilde yanıtladığına inanıyorum .

Kısa olmak gerekirse, alt problemlerinizden biri diğeri bittiğinde beklemek zorundaysa, çoklu kullanım iyidir (örneğin I / O ağır işlemlerinde); Aksine, alt sorunlarınız gerçekten aynı anda gerçekleşebiliyorsa, çok işlem önerilir. Ancak, çekirdek sayınızdan daha fazla işlem oluşturmazsınız.


3

Python belgeleri

Process vs Threads ve GIL hakkında temel Python dokümantasyon alıntılarını vurguladım: CPython'daki global tercüman kilidi (GIL) nedir?

Proses ve iplik deneyleri

Farkı daha somut bir şekilde göstermek için biraz kıyaslama yaptım.

Kıyaslamada, CPU ve IO'yu 8 hiper iplik üzerinde çeşitli sayıda iş parçacığı için zamanladım CPU . Her iş parçacığı için sağlanan iş her zaman aynıdır, böylece daha fazla iş parçacığı daha fazla toplam iş anlamına gelir.

Sonuçlar:

resim açıklamasını buraya girin

Verileri çizin .

Sonuç:

  • CPU bağlantılı işler için, muhtemelen GIL nedeniyle çoklu işlem her zaman daha hızlıdır

  • ES bağlı çalışma için. ikisi de aynı hızda

  • 8 hiper iş parçalı bir makinede olduğumdan, iş parçacıkları beklenen 8x yerine yaklaşık 4x'e kadar ölçeklendirilir.

    Beklenen 8x hıza ulaşan bir C POSIX CPU-bağlı çalışma ile kontrast: Zamanın çıktısında 'gerçek', 'kullanıcı' ve 'sys' ne anlama geliyor?

    YAPILACAKLAR: Bunun nedenini bilmiyorum, oyuna giren başka Python verimsizlikleri olmalı.

Test kodu:

#!/usr/bin/env python3

import multiprocessing
import threading
import time
import sys

def cpu_func(result, niters):
    '''
    A useless CPU bound function.
    '''
    for i in range(niters):
        result = (result * result * i + 2 * result * i * i + 3) % 10000000
    return result

class CpuThread(threading.Thread):
    def __init__(self, niters):
        super().__init__()
        self.niters = niters
        self.result = 1
    def run(self):
        self.result = cpu_func(self.result, self.niters)

class CpuProcess(multiprocessing.Process):
    def __init__(self, niters):
        super().__init__()
        self.niters = niters
        self.result = 1
    def run(self):
        self.result = cpu_func(self.result, self.niters)

class IoThread(threading.Thread):
    def __init__(self, sleep):
        super().__init__()
        self.sleep = sleep
        self.result = self.sleep
    def run(self):
        time.sleep(self.sleep)

class IoProcess(multiprocessing.Process):
    def __init__(self, sleep):
        super().__init__()
        self.sleep = sleep
        self.result = self.sleep
    def run(self):
        time.sleep(self.sleep)

if __name__ == '__main__':
    cpu_n_iters = int(sys.argv[1])
    sleep = 1
    cpu_count = multiprocessing.cpu_count()
    input_params = [
        (CpuThread, cpu_n_iters),
        (CpuProcess, cpu_n_iters),
        (IoThread, sleep),
        (IoProcess, sleep),
    ]
    header = ['nthreads']
    for thread_class, _ in input_params:
        header.append(thread_class.__name__)
    print(' '.join(header))
    for nthreads in range(1, 2 * cpu_count):
        results = [nthreads]
        for thread_class, work_size in input_params:
            start_time = time.time()
            threads = []
            for i in range(nthreads):
                thread = thread_class(work_size)
                threads.append(thread)
                thread.start()
            for i, thread in enumerate(threads):
                thread.join()
            results.append(time.time() - start_time)
        print(' '.join('{:.6e}'.format(result) for result in results))

GitHub akış yukarı + aynı dizine kod çizme .

Ubuntu 18.10, Python 3.6.7, CPU'lu bir Lenovo ThinkPad P51 dizüstü bilgisayarda test edildi: Intel Core i7-7820HQ CPU (4 çekirdek / 8 iplik), RAM: 2x Samsung M471A2K43BB1-CRC (2x 16GiB), SSD: Samsung MZVLB512HAJQ- 000L7 (3.000 MB / s).

Belirli bir zamanda hangi iş parçacıklarının çalıştığını görselleştirin

Bu yazı https://rohanvarma.me/GIL/ bana ve aynı target=argümanıthreading.Thread ile bir iş parçacığı zaman zaman bir geri arama çalıştırabileceğini öğretti multiprocessing.Process.

Bu, her seferinde tam olarak hangi iş parçacığının çalıştığını görüntülememizi sağlar. Bu yapıldığında, şöyle bir şey görürüz (bu grafiği hazırladım):

            +--------------------------------------+
            + Active threads / processes           +
+-----------+--------------------------------------+
|Thread   1 |********     ************             |
|         2 |        *****            *************|
+-----------+--------------------------------------+
|Process  1 |***  ************** ******  ****      |
|         2 |** **** ****** ** ********* **********|
+-----------+--------------------------------------+
            + Time -->                             +
            +--------------------------------------+

ki bunu gösterecekti:

  • dişler GIL tarafından tamamen serileştirilir
  • süreçler paralel olarak çalışabilir

1

Aşağıda, python 2.6.x için, diş açmanın IO'ya bağlı senaryolarda çoklu işlemden daha performanslı olduğu fikrini sorgulamaya çalışan bazı performans verileri bulunmaktadır. Bu sonuçlar 40 işlemcili bir IBM System x3650 M4 BD'den alınmıştır.

IO-Bound İşleme: Process Pool, Thread Pool'dan daha iyi performans gösterdi

>>> do_work(50, 300, 'thread','fileio')
do_work function took 455.752 ms

>>> do_work(50, 300, 'process','fileio')
do_work function took 319.279 ms

CPU-Sınırlı İşleme: Process Pool, Thread Pool'dan daha iyi performans gösterdi

>>> do_work(50, 2000, 'thread','square')
do_work function took 338.309 ms

>>> do_work(50, 2000, 'process','square')
do_work function took 287.488 ms

Bunlar titiz testler değil, ancak çok işlemciliğin diş çekme ile karşılaştırıldığında tamamen sağlam olmadığını söylüyorlar.

Yukarıdaki testler için etkileşimli python konsolunda kullanılan kod

from multiprocessing import Pool
from multiprocessing.pool import ThreadPool
import time
import sys
import os
from glob import glob

text_for_test = str(range(1,100000))

def fileio(i):
 try :
  os.remove(glob('./test/test-*'))
 except : 
  pass
 f=open('./test/test-'+str(i),'a')
 f.write(text_for_test)
 f.close()
 f=open('./test/test-'+str(i),'r')
 text = f.read()
 f.close()


def square(i):
 return i*i

def timing(f):
 def wrap(*args):
  time1 = time.time()
  ret = f(*args)
  time2 = time.time()
  print '%s function took %0.3f ms' % (f.func_name, (time2-time1)*1000.0)
  return ret
 return wrap

result = None

@timing
def do_work(process_count, items, process_type, method) :
 pool = None
 if process_type == 'process' :
  pool = Pool(processes=process_count)
 else :
  pool = ThreadPool(processes=process_count)
 if method == 'square' : 
  multiple_results = [pool.apply_async(square,(a,)) for a in range(1,items)]
  result = [res.get()  for res in multiple_results]
 else :
  multiple_results = [pool.apply_async(fileio,(a,)) for a in range(1,items)]
  result = [res.get()  for res in multiple_results]


do_work(50, 300, 'thread','fileio')
do_work(50, 300, 'process','fileio')

do_work(50, 2000, 'thread','square')
do_work(50, 2000, 'process','square')

Kodunuzu kullandım ( glob kısmını kaldırdım ) ve Python 2.6.6 ile bu ilginç sonuçları buldum:>>> do_work(50, 300, 'thread', 'fileio') --> 237.557 ms >>> do_work(50, 300, 'process', 'fileio') --> 323.963 ms >>> do_work(50, 2000, 'thread', 'square') --> 232.082 ms >>> do_work(50, 2000, 'process', 'square') --> 282.785 ms
Alan Garrido

-5

Sorunun çoğu Giulio Franco tarafından cevaplandı. Daha çok iş parçacıklı bir uygulama kullanmak için sizi çözümün doğru yoluna koyacağımı düşündüğüm tüketici-üretici sorununu daha ayrıntılı olarak ele alacağım.

fill_count = Semaphore(0) # items produced
empty_count = Semaphore(BUFFER_SIZE) # remaining space
buffer = Buffer()

def producer(fill_count, empty_count, buffer):
    while True:
        item = produceItem()
        empty_count.down();
        buffer.push(item)
        fill_count.up()

def consumer(fill_count, empty_count, buffer):
    while True:
        fill_count.down()
        item = buffer.pop()
        empty_count.up()
        consume_item(item)

Senkronizasyon ilkeleri hakkında daha fazla bilgi için:

 http://linux.die.net/man/7/sem_overview
 http://docs.python.org/2/library/threading.html

Sözde kod yukarıda. Daha fazla referans almak için üretici-tüketici sorununu araştırmanız gerektiğini düşünüyorum.


üzgünüm innosam, ama bu bana C ++ gibi görünüyor? bağlantılar için teşekkürler :)
lucacerone

Aslında, çoklu işlem ve çoklu iş parçacığının ardındaki fikirler dilden bağımsızdır. Çözüm yukarıdaki koda benzer olacaktır.
innosam

2
Bu C ++ değil; sözde kod (ya da C benzeri bir sözdizimiyle çoğunlukla dinamik olarak yazılan bir dil için kod. Bu söyleniyor, ben Python kullanıcılarına öğretmek için Python benzeri sözde kod yazmak daha yararlı olduğunu düşünüyorum. (Özellikle Python benzeri psuedocode beri sık sık çalıştırılabilir kod olduğu veya en azından ona yakın olduğu ortaya çıkıyor, bu C benzeri sahte kod için nadiren doğrudur…)
abarnert

Python benzeri sözde kod olarak yeniden yazdım (ayrıca küresel nesneleri kullanmak yerine OO ve geçen parametreleri kullanarak); bunun daha az net olduğunu düşünüyorsanız, geri dönmekten çekinmeyin.
abarnert

Ayrıca, Python stdlib'in tüm bu detayları tamamlayan senkronize bir kuyruğa sahip olduğunu ve iş parçacığı ve işlem havuzu API'lerini işleri daha da soyutladığını belirtmek gerekir. Senkronize kuyrukların kapakların altında nasıl çalıştığını kesinlikle anlamaya değer, ancak nadiren kendiniz yazmanız gerekir.
abarnert
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.