Apa perbedaan antara modul threading dan multiprocessing?

141

Saya belajar cara menggunakan threadingdan multiprocessingmodul dalam Python untuk menjalankan operasi tertentu secara paralel dan mempercepat kode saya.

Saya menemukan ini sulit (mungkin karena saya tidak memiliki latar belakang teoritis tentang hal itu) untuk memahami apa perbedaan antara threading.Thread()objek danmultiprocessing.Process() .

Juga, tidak sepenuhnya jelas bagi saya bagaimana membuat instance antrian pekerjaan dan hanya memiliki 4 (misalnya) dari mereka yang berjalan secara paralel, sementara yang lain menunggu sumber daya untuk membebaskan sebelum dieksekusi.

Saya menemukan contoh dalam dokumentasi yang jelas, tetapi tidak terlalu lengkap; segera setelah saya mencoba sedikit memperumit masalah, saya menerima banyak kesalahan aneh (seperti metode yang tidak bisa diacungi, dan sebagainya).

Jadi, kapan saya harus menggunakan threadingdanmultiprocessing modul?

Bisakah Anda menghubungkan saya dengan beberapa sumber yang menjelaskan konsep di balik dua modul ini dan bagaimana menggunakannya dengan benar untuk tugas-tugas kompleks?

lucacerone
sumber
Masih ada lagi, ada juga Threadmodul (disebut _threaddengan python 3.x). Sejujurnya, aku sendiri tidak pernah mengerti perbedaannya ...
Entahlah
3
@ Tidak Tahu: Seperti yang dikatakan Thread/ _threaddokumentasi secara eksplisit, "primitif tingkat rendah". Anda dapat menggunakannya untuk membuat objek sinkronisasi khusus, untuk mengontrol urutan gabungan dari utas, dll. Jika Anda tidak dapat membayangkan mengapa Anda harus menggunakannya, jangan menggunakannya, dan tetap menggunakannya threading.
abarnert

Jawaban:

260

Apa yang dikatakan Giulio Franco adalah benar untuk multithreading vs. multiprocessing secara umum .

Namun, Python * memiliki masalah tambahan: Ada Global Interpreter Lock yang mencegah dua utas dalam proses yang sama dari menjalankan kode Python pada saat yang sama. Ini berarti bahwa jika Anda memiliki 8 core, dan mengubah kode Anda untuk menggunakan 8 thread, itu tidak akan dapat menggunakan CPU 800% dan menjalankan 8x lebih cepat; itu akan menggunakan CPU 100% yang sama dan berjalan pada kecepatan yang sama. (Pada kenyataannya, ini akan berjalan sedikit lebih lambat, karena ada overhead tambahan dari threading, bahkan jika Anda tidak memiliki data bersama, tetapi abaikan itu untuk saat ini.)

Ada pengecualian untuk ini. Jika perhitungan berat kode Anda tidak benar-benar terjadi di Python, tetapi di beberapa pustaka dengan kode C kustom yang melakukan penanganan GIL yang tepat, seperti aplikasi numpy, Anda akan mendapatkan manfaat kinerja yang diharapkan dari threading. Hal yang sama berlaku jika perhitungan berat dilakukan oleh beberapa subproses yang Anda jalankan dan tunggu.

Lebih penting lagi, ada kasus di mana ini tidak masalah. Misalnya, server jaringan menghabiskan sebagian besar waktunya membaca paket dari jaringan, dan aplikasi GUI menghabiskan sebagian besar waktunya menunggu acara pengguna. Salah satu alasan untuk menggunakan utas di server jaringan atau aplikasi GUI adalah untuk memungkinkan Anda melakukan "tugas latar belakang" yang sudah berjalan lama tanpa menghentikan utas dari melanjutkan ke paket layanan jaringan atau acara GUI. Dan itu berfungsi dengan baik dengan utas Python. (Dalam istilah teknis, ini berarti utas Python memberi Anda konkurensi, meskipun mereka tidak memberi Anda paralelisme inti.)

Tetapi jika Anda menulis program yang terikat CPU dengan Python murni, menggunakan lebih banyak utas umumnya tidak membantu.

Menggunakan proses yang terpisah tidak memiliki masalah dengan GIL, karena setiap proses memiliki GIL yang terpisah. Tentu saja Anda masih memiliki semua pengorbanan yang sama antara utas dan proses seperti dalam bahasa lain — lebih sulit dan lebih mahal untuk berbagi data antar proses daripada antar utas, mungkin mahal untuk menjalankan sejumlah besar proses atau untuk membuat dan menghancurkan mereka sering, dll. Tapi GIL sangat membebani keseimbangan terhadap proses, dengan cara yang tidak benar untuk, katakanlah, C atau Java. Jadi, Anda akan lebih sering menggunakan multiprosesor dalam Python daripada di C atau Java.


Sementara itu, filosofi "baterai termasuk" Python membawa kabar baik: Sangat mudah untuk menulis kode yang dapat diubah-ubah antara utas dan proses dengan perubahan satu-liner.

Jika Anda mendesain kode Anda dalam hal "pekerjaan" mandiri yang tidak membagikan apa pun dengan pekerjaan lain (atau program utama) kecuali input dan output, Anda dapat menggunakan concurrent.futuresperpustakaan untuk menulis kode Anda di sekitar kumpulan utas seperti ini:

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

Anda bahkan bisa mendapatkan hasil dari pekerjaan itu dan meneruskannya ke pekerjaan selanjutnya, menunggu hal-hal dalam urutan eksekusi atau dalam urutan penyelesaian, dll .; baca bagian Futureobjek untuk detailnya.

Sekarang, jika ternyata program Anda terus-menerus menggunakan CPU 100%, dan menambahkan lebih banyak utas hanya membuatnya lebih lambat, maka Anda mengalami masalah GIL, jadi Anda perlu beralih ke proses. Yang harus Anda lakukan adalah mengubah baris pertama itu:

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

Satu-satunya peringatan nyata adalah bahwa argumen pekerjaan Anda dan nilai-nilai pengembalian harus acar (dan tidak mengambil terlalu banyak waktu atau ingatan untuk acar) untuk dapat digunakan lintas proses. Biasanya ini bukan masalah, tapi terkadang memang begitu.


Tetapi bagaimana jika pekerjaan Anda tidak bisa mandiri? Jika Anda dapat merancang kode Anda dalam hal pekerjaan yang menyampaikan pesan dari satu ke yang lain, itu masih cukup mudah. Anda mungkin harus menggunakan threading.Threadatau multiprocessing.Processbukannya mengandalkan kolam. Dan Anda harus membuat queue.Queueatau multiprocessing.Queueobjek secara eksplisit. (Ada banyak opsi lain — pipa, soket, file dengan kawanan, ... tetapi intinya adalah, Anda harus melakukan sesuatu secara manual jika sihir otomatis dari seorang Executor tidak cukup.)

Tetapi bagaimana jika Anda bahkan tidak bisa mengandalkan pesan yang lewat? Bagaimana jika Anda membutuhkan dua pekerjaan untuk keduanya mengubah struktur yang sama, dan melihat perubahan satu sama lain? Dalam hal ini, Anda perlu melakukan sinkronisasi manual (kunci, semaphore, kondisi, dll.) Dan, jika Anda ingin menggunakan proses, objek shared-memory eksplisit untuk boot. Ini terjadi ketika multithreading (atau multiprocessing) menjadi sulit. Jika Anda bisa menghindarinya, bagus; jika Anda tidak bisa, Anda harus membaca lebih banyak daripada yang dapat dimasukkan seseorang ke dalam jawaban SO.


Dari komentar, Anda ingin tahu apa yang berbeda antara utas dan proses dalam Python. Sungguh, jika Anda membaca jawaban Giulio Franco dan milik saya serta semua tautan kami, itu akan mencakup semuanya ... tetapi ringkasan pasti akan berguna, jadi begini:

  1. Utas berbagi data secara default; proses tidak.
  2. Sebagai konsekuensi dari (1), mengirim data antar proses umumnya membutuhkan pengawetan dan pembongkaran. **
  3. Sebagai konsekuensi lain dari (1), berbagi data secara langsung antara proses biasanya mengharuskan memasukkannya ke dalam format tingkat rendah seperti Nilai, Array, dan ctypesjenis.
  4. Proses tidak tunduk pada GIL.
  5. Pada beberapa platform (terutama Windows), proses jauh lebih mahal untuk dibuat dan dihancurkan.
  6. Ada beberapa batasan ekstra pada proses, beberapa di antaranya berbeda pada platform yang berbeda. Lihat panduan Pemrograman untuk detailnya.
  7. The threadingmodul tidak memiliki beberapa fitur dari multiprocessingmodul. (Anda dapat menggunakan multiprocessing.dummyuntuk mendapatkan sebagian besar API yang hilang di atas utas, atau Anda dapat menggunakan modul tingkat yang lebih tinggi suka concurrent.futuresdan tidak khawatir tentang hal itu.)

* Sebenarnya bukan Python, bahasa, yang memiliki masalah ini, tetapi CPython, implementasi "standar" dari bahasa itu. Beberapa implementasi lain tidak memiliki GIL, seperti Jython.

** Jika Anda menggunakan metode fork start untuk multi-pemrosesan — yang dapat Anda lakukan di sebagian besar platform non-Windows — setiap proses anak mendapatkan sumber daya apa pun yang dimiliki orang tua ketika anak dimulai, yang bisa menjadi cara lain untuk meneruskan data kepada anak-anak.

abarnert
sumber
terima kasih, tapi saya tidak yakin saya mengerti segalanya. Pokoknya saya mencoba melakukannya sedikit untuk tujuan belajar, dan sedikit karena dengan penggunaan thread yang naif saya mengurangi separuh kecepatan kode saya (mulai lebih dari 1000 utas pada saat yang sama, masing-masing memanggil aplikasi eksternal .. ini jenuh cpu, namun ada peningkatan x2 dalam kecepatan). Saya pikir mengelola utas secara cerdas mungkin benar-benar meningkatkan kecepatan kode saya ..
lucacerone
3
@ LucaCerone: Ah, jika kode Anda menghabiskan sebagian besar waktunya menunggu pada program eksternal, maka ya, itu akan mendapat manfaat dari threading. Poin yang bagus. Biarkan saya mengedit jawaban untuk menjelaskan itu.
abarnert
2
@LucaCerone: Sementara itu, bagian apa yang tidak Anda mengerti? Tanpa mengetahui tingkat pengetahuan yang Anda mulai, sulit untuk menulis jawaban yang baik ... tetapi dengan beberapa umpan balik, mungkin kita dapat menemukan sesuatu yang membantu Anda dan pembaca di masa depan juga.
abarnert
3
@LucaCerone Anda harus membaca PEP untuk multi-pemrosesan di sini . Ini memberi timing dan contoh thread vs multiprosesing.
mr2ert
1
@ LucaCerone: Jika objek yang terikat metode tidak memiliki keadaan kompleks, solusi paling sederhana untuk masalah pengawetan adalah menulis fungsi pembungkus bodoh yang menghasilkan objek dan memanggil metode. Jika memang memiliki keadaan kompleks, maka Anda mungkin harus membuatnya picklable (yang cukup mudah; pickledokumen menjelaskannya), dan kemudian yang terburuk adalah pembungkus bodoh Anda def wrapper(obj, *args): return obj.wrapper(*args).
abarnert
32

Beberapa utas dapat ada dalam satu proses tunggal. Utas yang termasuk dalam proses yang sama berbagi area memori yang sama (dapat membaca dan menulis ke variabel yang sama, dan dapat saling mengganggu). Sebaliknya, berbagai proses hidup di area memori yang berbeda, dan masing-masing memiliki variabel sendiri. Untuk berkomunikasi, proses harus menggunakan saluran lain (file, pipa atau soket).

Jika Anda ingin memparalelkan perhitungan, Anda mungkin perlu multithreading, karena Anda mungkin ingin utas untuk bekerja sama pada memori yang sama.

Berbicara tentang kinerja, utas lebih cepat untuk dibuat dan dikelola daripada proses (karena OS tidak perlu mengalokasikan area memori virtual yang sama sekali baru), dan komunikasi antar-utas biasanya lebih cepat daripada komunikasi antar-proses. Tetapi utas lebih sulit diprogram. Utas dapat saling mengganggu, dan dapat menulis ke memori satu sama lain, tetapi cara ini terjadi tidak selalu jelas (karena beberapa faktor, terutama instruksi pemesanan ulang dan caching memori), sehingga Anda akan memerlukan sinkronisasi primitif untuk mengontrol akses ke variabel Anda.

Giulio Franco
sumber
12
Ini kehilangan beberapa informasi yang sangat penting tentang GIL, yang membuatnya menyesatkan.
abarnert
1
@ mr2ert: Ya, itu informasi yang sangat penting secara singkat. :) Tapi ini sedikit lebih rumit dari itu, itulah sebabnya saya menulis jawaban terpisah.
abarnert
2
Saya pikir saya berkomentar mengatakan bahwa @abarnert benar, dan saya lupa tentang GIL dalam menjawab di sini. Jadi jawaban ini salah, Anda sebaiknya tidak membenarkannya.
Giulio Franco
6
Saya menurunkan jawaban ini karena masih tidak menjawab sama sekali apa perbedaan antara Python threadingdan multiprocessing.
Antti Haapala
Saya telah membaca bahwa ada GIL untuk setiap proses. Tetapi apakah semua proses menggunakan interpreter python yang sama atau apakah ada interpreter terpisah per utas?
variabel
3

Saya yakin tautan ini menjawab pertanyaan Anda dengan cara yang elegan.

Singkatnya, jika salah satu sub-masalah Anda harus menunggu sementara yang lain selesai, multithreading baik (dalam operasi berat I / O, misalnya); sebaliknya, jika sub-masalah Anda benar-benar dapat terjadi pada saat yang bersamaan, disarankan untuk melakukan multi-pemrosesan. Namun, Anda tidak akan membuat lebih banyak proses daripada jumlah inti Anda.

ehfaafzv
sumber
3

Kutipan dokumentasi Python

Saya telah menyoroti kutipan dokumentasi Python kunci tentang Process vs Threads dan GIL di: Apa kunci juru bahasa global (GIL) di CPython?

Percobaan proses vs utas

Saya melakukan sedikit pembandingan untuk menunjukkan perbedaan lebih konkret.

Dalam benchmark, saya menghitung waktu kerja CPU dan IO untuk berbagai jumlah utas pada CPU 8 hyperthread . Pekerjaan yang disediakan per utas selalu sama, sehingga lebih banyak utas berarti lebih banyak total pekerjaan yang disediakan.

Hasilnya adalah:

masukkan deskripsi gambar di sini

Plot data .

Kesimpulan:

  • untuk pekerjaan yang terikat CPU, multi-pemrosesan selalu lebih cepat, mungkin karena GIL

  • untuk pekerjaan terikat IO. keduanya persis kecepatan yang sama

  • utas hanya meningkatkan hingga sekitar 4x daripada yang diharapkan 8x karena saya menggunakan mesin 8 hyperthread.

    Bandingkan dengan C POSIX yang bekerja dengan CPU yang mencapai kecepatan 8x yang diharapkan: Apa arti 'nyata', 'pengguna' dan 'sistem' dalam output waktu (1)?

    TODO: Saya tidak tahu alasannya, pasti ada inefisiensi Python lain yang ikut bermain.

Kode uji:

#!/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 kode upstream + plotting pada direktori yang sama .

Diuji pada Ubuntu 18.10, Python 3.6.7, dalam laptop Lenovo ThinkPad P51 dengan CPU: Intel Core i7-7820HQ CPU (4 core / 8 threads), RAM: 2x Samsung M471A2K43BB1-CRC (2x 16GiB), SSD: Samsung MZVLB512HAJQ- 000L7 (3.000 MB / s).

Visualisasikan utas mana yang berjalan pada waktu tertentu

Posting ini https://rohanvarma.me/GIL/ mengajari saya bahwa Anda dapat menjalankan panggilan balik setiap kali utas dijadwalkan dengan target=argumenthreading.Thread dan sama untuk multiprocessing.Process.

Ini memungkinkan kami untuk melihat thread mana yang berjalan pada setiap waktu. Ketika ini selesai, kita akan melihat sesuatu seperti (saya membuat grafik khusus ini):

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

yang akan menunjukkan bahwa:

  • utas sepenuhnya serial oleh GIL
  • proses dapat berjalan secara paralel
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
sumber
1

Berikut adalah beberapa data kinerja untuk python 2.6.x yang meminta untuk mempertanyakan gagasan bahwa threading lebih berkinerja lebih daripada multiprocessing dalam skenario IO-terikat. Hasil ini berasal dari Sistem IBM 40-prosesor x3650 M4 BD.

Pemrosesan IO-Bound: Pool Proses berkinerja lebih baik daripada Thread Pool

>>> 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-Bound Processing: Process Pool tampil lebih baik daripada Thread Pool

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

Ini bukan tes yang ketat, tetapi mereka mengatakan kepada saya bahwa multi-pemrosesan tidak sepenuhnya tidak sebanding dibandingkan dengan threading.

Kode yang digunakan dalam konsol python interaktif untuk pengujian di atas

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')
Mario Aguilera
sumber
Saya telah menggunakan kode Anda (menghapus bagian glob ) dan telah menemukan hasil yang menarik ini dengan Python 2.6.6:>>> 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

Ya, sebagian besar pertanyaan dijawab oleh Giulio Franco. Saya akan menjelaskan lebih lanjut tentang masalah konsumen-produsen, yang saya kira akan menempatkan Anda di jalur yang benar untuk solusi Anda menggunakan aplikasi multithreaded.

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)

Anda dapat membaca lebih lanjut tentang sinkronisasi primitif dari:

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

Kode semu di atas. Saya kira Anda harus mencari masalah produsen-konsumen untuk mendapatkan lebih banyak referensi.

innosam
sumber
maaf innosam, tapi ini sepertinya C ++ bagi saya? terima kasih atas
tautannya
Sebenarnya, ide di balik multi-pemrosesan dan multithreading adalah bahasa yang independen. Solusinya akan mirip dengan kode di atas.
innosam
2
Ini bukan C ++; itu pseudocode (atau kode untuk bahasa yang sebagian besar diketik secara dinamis dengan sintaks seperti C. Itu dikatakan, saya pikir itu lebih berguna untuk menulis pseudocode mirip-Python untuk mengajar pengguna Python. ternyata menjadi kode runnable, atau setidaknya dekat dengannya, yang jarang benar untuk pseudocode C-like ...)
abarnert
Saya telah menulis ulang sebagai pseudocode seperti-Python (juga menggunakan OO dan melewati parameter daripada menggunakan objek global); jangan ragu untuk kembali jika Anda berpikir itu membuat segalanya menjadi kurang jelas.
abarnert
Juga, perlu dicatat bahwa stdlib Python memiliki antrian yang disinkronkan yang membungkus semua detail ini, dan utasnya serta kumpulan proses API abstrak hal-hal yang lebih jauh. Jelas layak untuk memahami bagaimana antrian yang disinkronkan bekerja di bawah selimut, tetapi Anda jarang perlu menulis sendiri.
abarnert