Mengapa, kapan dan bagaimana menggunakan multithreading dan multiprocessing dengan Python

Salute, Khabrovsk. Saat ini, OTUS telah membuka satu set untuk kursus Pembelajaran Mesin , sehubungan dengan ini kami telah menerjemahkan untuk Anda satu "dongeng" yang sangat menarik. Pergilah.




Suatu hari, di galaksi yang jauh, jauh ... Seorang

penyihir yang bijaksana dan kuat tinggal di sebuah desa kecil di tengah padang pasir. Dan nama mereka adalah Dumbledalf. Dia tidak hanya bijak dan kuat, tetapi juga membantu orang-orang yang datang dari negeri yang jauh untuk meminta bantuan seorang penyihir. Kisah kami dimulai ketika seorang musafir membawa gulungan sihir. Pelancong tidak tahu apa yang ada di gulungan itu, dia hanya tahu bahwa jika ada yang bisa mengungkapkan semua rahasia gulungan itu, maka ini adalah Dumbledalf.

Bab 1: Single-Threaded, Proses-Tunggal


Jika Anda belum menebak, saya menggambar analogi dengan prosesor dan fungsinya. Wisaya kami adalah prosesor, dan sebuah gulir adalah daftar tautan yang mengarah pada kekuatan dan pengetahuan Python untuk menguasainya.

Pikiran pertama penyihir yang menguraikan daftar tanpa kesulitan adalah mengirim teman yang setia (Garrigorn? Aku tahu, aku tahu itu terdengar mengerikan) ke setiap tempat yang dijelaskan dalam gulungan untuk menemukan dan membawa apa yang bisa dia temukan di sana.

In [1]:
import urllib.request
from concurrent.futures import ThreadPoolExecutor
In [2]:
urls = [
  'http://www.python.org',
  'https://docs.python.org/3/',
  'https://docs.python.org/3/whatsnew/3.7.html',
  'https://docs.python.org/3/tutorial/index.html',
  'https://docs.python.org/3/library/index.html',
  'https://docs.python.org/3/reference/index.html',
  'https://docs.python.org/3/using/index.html',
  'https://docs.python.org/3/howto/index.html',
  'https://docs.python.org/3/installing/index.html',
  'https://docs.python.org/3/distributing/index.html',
  'https://docs.python.org/3/extending/index.html',
  'https://docs.python.org/3/c-api/index.html',
  'https://docs.python.org/3/faq/index.html'
  ]
In [3]:
%%time

results = []
for url in urls:
    with urllib.request.urlopen(url) as src:
        results.append(src)

CPU times: user 135 ms, sys: 283 µs, total: 135 ms
Wall time: 12.3 s
In [ ]:

Seperti yang Anda lihat, kami cukup mengulangi URL satu per satu menggunakan for for dan membaca jawabannya. Berkat %% waktu dan keajaiban IPython , kita dapat melihat bahwa butuh sekitar 12 detik dengan internet sedih saya.

Bab 2: Multithreading


Bukan tanpa alasan bahwa penyihir itu terkenal karena kebijaksanaannya, ia dengan cepat berhasil menemukan cara yang jauh lebih efektif. Alih-alih mengirim satu orang ke setiap tempat secara berurutan, mengapa tidak mengumpulkan sepasukan rekanan yang dapat diandalkan dan mengirim mereka ke berbagai belahan dunia secara bersamaan! Wizard akan dapat menyatukan semua pengetahuan yang mereka bawa sekaligus!

Itu benar, alih-alih melihat daftar dalam satu lingkaran secara berurutan, kita dapat menggunakan multithreading untuk mengakses beberapa URL sekaligus.

In [1]:
import urllib.request
from concurrent.futures import ThreadPoolExecutor
In [2]:
urls = [
  'http://www.python.org',
  'https://docs.python.org/3/',
  'https://docs.python.org/3/whatsnew/3.7.html',
  'https://docs.python.org/3/tutorial/index.html',
  'https://docs.python.org/3/library/index.html',
  'https://docs.python.org/3/reference/index.html',
  'https://docs.python.org/3/using/index.html',
  'https://docs.python.org/3/howto/index.html',
  'https://docs.python.org/3/installing/index.html',
  'https://docs.python.org/3/distributing/index.html',
  'https://docs.python.org/3/extending/index.html',
  'https://docs.python.org/3/c-api/index.html',
  'https://docs.python.org/3/faq/index.html'
  ]
In [4]:
%%time

with ThreadPoolExecutor(4) as executor:
    results = executor.map(urllib.request.urlopen, urls)

CPU times: user 122 ms, sys: 8.27 ms, total: 130 ms
Wall time: 3.83 s
In [5]:
%%time

with ThreadPoolExecutor(8) as executor:
    results = executor.map(urllib.request.urlopen, urls)

CPU times: user 122 ms, sys: 14.7 ms, total: 137 ms
Wall time: 1.79 s
In [6]:
%%time

with ThreadPoolExecutor(16) as executor:
    results = executor.map(urllib.request.urlopen, urls)

CPU times: user 143 ms, sys: 3.88 ms, total: 147 ms
Wall time: 1.32 s
In [ ]:

Jauh lebih baik! Hampir seperti ... sihir. Menggunakan banyak utas dapat secara signifikan mempercepat banyak tugas I / O. Dalam kasus saya, sebagian besar waktu yang dihabiskan untuk membaca URL adalah karena latensi jaringan. Program yang terkait dengan I / O menghabiskan sebagian besar hidup mereka menunggu, Anda dapat menebaknya, untuk input atau output (seperti seorang penyihir menunggu teman-temannya untuk pergi ke tempat-tempat dari gulungan dan kembali). Ini dapat berupa input / output dari jaringan, database, file, atau dari pengguna. I / O seperti itu biasanya membutuhkan banyak waktu, karena sumber mungkin perlu melakukan preprocessing sebelum mentransfer data ke I / O. Sebagai contoh, prosesor akan menghitung jauh lebih cepat daripada koneksi jaringan akan mengirimkan data (dengan kecepatan kira-kiraseperti Flash vs nenekmu).

Catatan : multithreadingbisa sangat berguna dalam tugas-tugas seperti membersihkan halaman web.

Bab 3: Proses banyak


Tahun-tahun berlalu, ketenaran penyihir baik tumbuh, dan dengan itu tumbuh kecemburuan seorang penyihir gelap yang tidak memihak (Sarumort? Atau mungkin Volandeman?). Dipersenjatai dengan kelicikan yang tak terukur dan didorong oleh rasa iri, penyihir gelap itu mengutuk Dumbledalf. Ketika kutukan menyusulnya, Dumbledalf menyadari bahwa dia hanya memiliki beberapa saat untuk menangkalnya. Putus asa, ia mencari-cari buku mantra dan dengan cepat menemukan satu mantra balasan yang seharusnya bisa digunakan. Satu-satunya masalah adalah penyihir itu perlu menghitung jumlah semua bilangan prima kurang dari 1.000.000 Mantra yang aneh, tentu saja, tetapi apa yang kita miliki.

Penyihir itu tahu bahwa menghitung nilai akan sepele jika dia punya cukup waktu, tetapi dia tidak memiliki kemewahan itu. Terlepas dari kenyataan bahwa ia adalah penyihir hebat, namun ia dibatasi oleh kemanusiaannya dan dapat memeriksa kesederhanaan hanya satu nomor pada suatu waktu. Jika dia memutuskan untuk menambahkan bilangan prima satu demi satu, itu akan memakan waktu terlalu banyak. Ketika hanya ada beberapa detik yang tersisa sebelum menerapkan mantra balasan, dia tiba-tiba teringat mantra multi - pemrosesan , yang telah dia pelajari dari gulungan sihir bertahun-tahun yang lalu. Mantra ini akan memungkinkannya untuk menyalin dirinya sendiri untuk mendistribusikan angka di antara salinannya dan memeriksa beberapa sekaligus. Dan pada akhirnya, yang perlu dia lakukan hanyalah menjumlahkan angka yang akan dia dan salinannya temukan.

In [1]:
from multiprocessing import Pool
In [2]:
def if_prime(x):
    if x <= 1:
        return 0
    elif x <= 3:
        return x
    elif x % 2 == 0 or x % 3 == 0:
        return 0
    i = 5
    while i**2 <= x:
        if x % i == 0 or x % (i + 2) == 0:
            return 0
        i += 6
    return x
In [17]:
%%time

answer = 0

for i in range(1000000):
    answer += if_prime(i)

CPU times: user 3.48 s, sys: 0 ns, total: 3.48 s
Wall time: 3.48 s
In [18]:
%%time

if __name__ == '__main__':
    with Pool(2) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

CPU times: user 114 ms, sys: 4.07 ms, total: 118 ms
Wall time: 1.91 s
In [19]:
%%time

if __name__ == '__main__':
    with Pool(4) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

CPU times: user 99.5 ms, sys: 30.5 ms, total: 130 ms
Wall time: 1.12 s
In [20]:
%%timeit

if __name__ == '__main__':
    with Pool(8) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

729 ms ± 3.02 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [21]:
%%timeit

if __name__ == '__main__':
    with Pool(16) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

512 ms ± 39.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [22]:
%%timeit

if __name__ == '__main__':
    with Pool(32) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

518 ms ± 13.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [23]:
%%timeit

if __name__ == '__main__':
    with Pool(64) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

621 ms ± 10.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [ ]:

Prosesor modern memiliki lebih dari satu inti, sehingga kita dapat mempercepat tugas menggunakan multiprocess pengolahan modul multiprocessing . Tugas yang terkait dengan prosesor adalah program yang sebagian besar waktu kerjanya melakukan perhitungan pada prosesor (perhitungan matematis sepele, pemrosesan gambar, dll.). Jika perhitungan dapat dilakukan secara independen satu sama lain, kami memiliki kesempatan untuk membaginya di antara inti prosesor yang tersedia, sehingga memperoleh peningkatan yang signifikan dalam kecepatan pemrosesan.

Yang harus Anda lakukan adalah:

  1. Tentukan fungsi yang akan diterapkan.
  2. Mempersiapkan daftar elemen yang akan diterapkan fungsinya;
  3. multiprocessing.Pool. , Pool(), . with , .
  4. Pool map. map , , .

Catatan : Fungsi dapat didefinisikan untuk melakukan tugas apa pun yang dapat dilakukan secara paralel. Misalnya, suatu fungsi dapat berisi kode untuk menulis hasil perhitungan ke file.

Jadi mengapa kita harus berpisahmultiprocessingdanmultithreading? Jika Anda pernah mencoba untuk meningkatkan kinerja tugas pada prosesor menggunakan multithreading, hasilnya justru sebaliknya. Itu mengerikan! Mari kita lihat bagaimana itu terjadi.

Sama seperti penyihir dibatasi oleh sifat manusianya dan hanya dapat menghitung satu angka per unit waktu, Python datang dengan sesuatu yang disebut Global Interpreter Lock (GIL) . Python senang membiarkan Anda menelurkan sebanyak mungkin utas yang Anda inginkan, tetapi GILmemastikan bahwa hanya satu dari utas ini yang akan dieksekusi pada waktu tertentu.

Untuk tugas yang berkaitan dengan I / O, situasi ini benar-benar normal. Satu utas mengirimkan permintaan ke satu URL dan menunggu tanggapan, hanya utas ini yang bisa diganti oleh yang lain, yang akan mengirim permintaan lain ke URL yang berbeda. Karena utas tidak perlu melakukan apa pun sampai ia menerima respons, maka tidak ada bedanya bahwa pada waktu tertentu hanya satu utas yang berjalan.

Untuk tugas yang dilakukan pada prosesor, memiliki beberapa utas hampir sama tidak berguna dengan puting pada baju besi. Karena hanya satu utas yang dapat dieksekusi per unit waktu, bahkan jika Anda menghasilkan beberapa utas, masing-masing akan dialokasikan nomor untuk memeriksa kesederhanaan, prosesor masih akan bekerja dengan hanya satu utas. Bahkan, angka-angka ini masih akan diperiksa satu per satu. Dan overhead bekerja dengan banyak utas akan membantu mengurangi kinerja, yang bisa Anda amati saat digunakan multithreadingdalam tugas yang dilakukan pada prosesor.

Untuk mengatasi "pembatasan" ini, kami menggunakan modulmultiprocessing. Alih-alih menggunakan utas, kegunaan multi-proses, seperti yang Anda katakan ... beberapa proses. Setiap proses mendapatkan juru bahasa dan ruang memori pribadi, sehingga GIL tidak akan membatasi Anda. Bahkan, setiap proses akan menggunakan inti prosesor sendiri dan bekerja dengan nomor uniknya sendiri, dan ini akan dilakukan bersamaan dengan pekerjaan proses lainnya. Betapa manisnya mereka!

Anda mungkin memperhatikan bahwa beban CPU akan lebih tinggi ketika Anda menggunakannya multiprocessingdibandingkan dengan yang biasa untuk loop atau bahkan multithreading. Ini terjadi karena program Anda tidak menggunakan satu inti, tetapi beberapa inti. Dan ini bagus!

ingat bahwamultiprocessingIni memiliki overhead sendiri dalam mengelola berbagai proses, yang biasanya lebih serius daripada biaya multithreading. (Multiprocessing memunculkan penerjemah terpisah dan menetapkan area ingatannya sendiri untuk setiap proses, jadi ya!) Artinya, lebih baik menggunakan versi ringan multithreading ketika Anda ingin keluar dengan cara ini (ingat tentang tugas yang terkait dengan I / O). Tetapi ketika perhitungan pada prosesor menjadi hambatan, waktu modul datang multiprocessing. Tetapi ingatlah bahwa dengan kekuatan besar datanglah tanggung jawab besar.

Jika Anda menelurkan lebih banyak proses daripada yang dapat diproses prosesor Anda per unit waktu, Anda akan melihat bahwa kinerja akan mulai menurun. Ini terjadi karena sistem operasi perlu melakukan lebih banyak pekerjaan dengan mengocok proses antara inti prosesor, karena ada lebih banyak proses. Pada kenyataannya, semuanya bisa menjadi lebih rumit daripada yang saya katakan hari ini, tetapi saya menyampaikan gagasan utama. Misalnya, pada sistem saya, kinerja akan turun ketika jumlah proses adalah 16. Ini karena hanya ada 16 core logis di prosesor saya.

Bab 4: Kesimpulan


  • Dalam tugas yang berkaitan dengan I / O, itu multithreadingdapat meningkatkan kinerja.
  • Dalam tugas-tugas yang terkait dengan I / O, multiprocessingini juga dapat meningkatkan produktivitas, tetapi biayanya, lebih tinggi daripada saat menggunakan multithreading.
  • Keberadaan Python GIL menjelaskan kepada kami bahwa pada waktu tertentu dalam suatu program, hanya satu utas yang dapat dijalankan.
  • Dalam tugas yang terkait dengan prosesor, penggunaan multithreadingdapat mengurangi kinerja.
  • Dalam tugas-tugas terkait prosesor, penggunaan multiprocessingdapat meningkatkan kinerja.
  • Penyihir luar biasa!

Di situlah kita akan mengakhiri pengantar kami ke multithreadingdan multiprocessingdengan Python hari ini . Sekarang, pergilah dan menangkan!



"Pemodelan COVID-19 menggunakan analisis grafik dan parsing data terbuka." Pelajaran gratis.

All Articles