Greenlet Vs. Utas

141

Saya baru mengenal gevents dan greenlets. Saya menemukan beberapa dokumentasi yang bagus tentang cara bekerja dengan mereka, tetapi tidak ada yang memberi saya pembenaran tentang bagaimana dan kapan saya harus menggunakan greenlets!

  • Apa yang benar-benar mereka kuasai?
  • Apakah ide yang baik untuk menggunakannya di server proxy atau tidak?
  • Kenapa tidak utas?

Apa yang saya tidak yakin tentang bagaimana mereka bisa memberikan kita konkurensi jika mereka pada dasarnya adalah co-rutinitas.

Rsh
sumber
1
@Imran Ini tentang greenthreads di Jawa. Pertanyaan saya adalah tentang greenlet dengan Python. Apakah saya melewatkan sesuatu?
Rsh
Afaik, utas dengan python sebenarnya tidak benar-benar bersamaan karena kunci juru bahasa global. Jadi itu akan memanas untuk membandingkan overhead dari kedua solusi. Meskipun saya mengerti bahwa ada beberapa implementasi python, jadi ini mungkin tidak berlaku untuk semuanya.
didierc
3
@didierc CPython (dan PyPy seperti sekarang) tidak akan menginterpretasikan kode Python (byte) secara paralel (yaitu, benar-benar secara fisik pada saat yang sama pada dua core CPU yang berbeda). Namun, tidak semua yang dilakukan oleh program Python berada di bawah GIL (contoh umum adalah syscalls termasuk fungsi I / O dan C yang sengaja melepaskan GIL), dan a threading.Threadsebenarnya adalah utas OS dengan semua konsekuensi. Jadi sebenarnya tidak sesederhana itu. Ngomong-ngomong, Jython tidak memiliki GIL AFAIK dan PyPy berusaha untuk menyingkirkannya juga.

Jawaban:

204

Greenlets memberikan konkurensi tetapi tidak paralelisme. Concurrency adalah ketika kode dapat berjalan secara independen dari kode lain. Paralelisme adalah eksekusi kode konkuren secara bersamaan. Paralelisme sangat berguna ketika ada banyak pekerjaan yang harus dilakukan di userspace, dan itu biasanya hal-hal yang berat CPU. Concurrency berguna untuk memecahkan masalah, memungkinkan berbagai bagian dijadwalkan dan dikelola lebih mudah secara paralel.

Greenlets benar-benar bersinar dalam pemrograman jaringan di mana interaksi dengan satu soket dapat terjadi secara terpisah dari interaksi dengan soket lainnya. Ini adalah contoh klasik konkurensi. Karena setiap greenlet berjalan dalam konteksnya sendiri, Anda dapat terus menggunakan API sinkron tanpa threading. Ini bagus karena utas sangat mahal dalam hal memori virtual dan overhead kernel, sehingga konkurensi yang dapat Anda lakukan dengan utas jauh lebih sedikit. Selain itu, threading dengan Python lebih mahal dan lebih terbatas dari biasanya karena GIL. Alternatif untuk konkurensi biasanya proyek-proyek seperti Twisted, libevent, libuv, node.js dll, di mana semua kode Anda berbagi konteks eksekusi yang sama, dan mendaftarkan penangan acara.

Merupakan ide yang bagus untuk menggunakan greenlets (dengan dukungan jaringan yang sesuai seperti melalui gevent) untuk menulis proxy, karena penanganan permintaan Anda dapat dieksekusi secara independen dan harus ditulis seperti itu.

Greenlets memberikan konkurensi untuk alasan yang saya berikan sebelumnya. Konkurensi bukanlah paralelisme. Dengan menyembunyikan registrasi acara dan melakukan penjadwalan untuk Anda pada panggilan yang biasanya akan memblokir utas saat ini, proyek-proyek seperti gevent mengungkapkan konkurensi ini tanpa memerlukan perubahan ke API asinkron, dan dengan biaya yang jauh lebih murah untuk sistem Anda.

Matt Joiner
sumber
1
Terima kasih, hanya dua pertanyaan kecil: 1) Apakah mungkin untuk menggabungkan solusi ini dengan multi-pemrosesan untuk mencapai throughput yang lebih tinggi? 2) Saya masih tidak tahu mengapa pernah menggunakan utas? Bisakah kita menganggapnya sebagai implementasi konkuren yang naif dan dasar dalam pustaka standar python?
Rsh
6
1) Ya, tentu saja. Anda seharusnya tidak melakukan ini sebelum waktunya, tetapi karena sejumlah faktor di luar cakupan pertanyaan ini, memiliki beberapa proses melayani permintaan akan memberi Anda hasil yang lebih tinggi. 2) Utas OS dijadwalkan sebelumnya, dan sepenuhnya diparalelkan secara default. Mereka adalah default dalam Python karena Python memperlihatkan antarmuka threading asli, dan utas adalah penyebut umum terbaik yang didukung dan terendah untuk paralelisme dan konkurensi dalam sistem operasi modern.
Matt Joiner
6
Saya harus menyebutkan bahwa Anda bahkan tidak boleh menggunakan greenlets sampai utas tidak memuaskan (biasanya ini terjadi karena jumlah koneksi simultan yang Anda tangani, dan entah jumlah utas atau GIL memberi Anda kesedihan), dan bahkan maka hanya jika tidak ada opsi lain yang tersedia untuk Anda. Pustaka standar Python, dan sebagian besar pustaka pihak ketiga mengharapkan konkurensi dicapai melalui utas, sehingga Anda mungkin mendapatkan perilaku aneh jika Anda menyediakannya melalui greenlets.
Matt Joiner
@ Mbattoin Saya memiliki fungsi di bawah ini yang membaca file besar untuk menghitung jumlah md5. bagaimana saya bisa menggunakan gevent dalam hal ini untuk membaca lebih cepat import hashlib def checksum_md5(filename): md5 = hashlib.md5() with open(filename,'rb') as f: for chunk in iter(lambda: f.read(8192), b''): md5.update(chunk) return md5.digest()
Soumya
18

Mengambil jawaban @ Max dan menambahkan beberapa relevansi untuk penskalaan, Anda dapat melihat perbedaannya. Saya mencapai ini dengan mengubah URL yang akan diisi sebagai berikut:

URLS_base = ['www.google.com', 'www.example.com', 'www.python.org', 'www.yahoo.com', 'www.ubc.ca', 'www.wikipedia.org']
URLS = []
for _ in range(10000):
    for url in URLS_base:
        URLS.append(url)

Saya harus keluar versi multiproses karena jatuh sebelum saya punya 500; tetapi pada 10.000 iterasi:

Using gevent it took: 3.756914
-----------
Using multi-threading it took: 15.797028

Jadi Anda bisa melihat ada beberapa perbedaan yang signifikan dalam I / O menggunakan gevent

TemporalBeing
sumber
4
itu sepenuhnya salah untuk menelurkan 60000 utas asli atau proses untuk menyelesaikan pekerjaan dan tes ini tidak menunjukkan apa-apa (juga apakah Anda mengambil waktu istirahat dari panggilan gevent.joinall ()?). Coba gunakan kumpulan utas sekitar 50 utas, lihat jawaban saya: stackoverflow.com/a/51932442/34549
zzzeek
9

Mengoreksi jawaban @TemporalBeing di atas, greenlets tidak "lebih cepat" dari utas dan itu adalah teknik pemrograman yang salah untuk menelurkan 60000 utas untuk memecahkan masalah konkurensi, sekelompok kecil utas bukannya sesuai. Berikut ini adalah perbandingan yang lebih masuk akal (dari posting reddit saya dalam menanggapi orang-orang yang mengutip posting SO ini).

import gevent
from gevent import socket as gsock
import socket as sock
import threading
from datetime import datetime


def timeit(fn, URLS):
    t1 = datetime.now()
    fn()
    t2 = datetime.now()
    print(
        "%s / %d hostnames, %s seconds" % (
            fn.__name__,
            len(URLS),
            (t2 - t1).total_seconds()
        )
    )


def run_gevent_without_a_timeout():
    ip_numbers = []

    def greenlet(domain_name):
        ip_numbers.append(gsock.gethostbyname(domain_name))

    jobs = [gevent.spawn(greenlet, domain_name) for domain_name in URLS]
    gevent.joinall(jobs)
    assert len(ip_numbers) == len(URLS)


def run_threads_correctly():
    ip_numbers = []

    def process():
        while queue:
            try:
                domain_name = queue.pop()
            except IndexError:
                pass
            else:
                ip_numbers.append(sock.gethostbyname(domain_name))

    threads = [threading.Thread(target=process) for i in range(50)]

    queue = list(URLS)
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    assert len(ip_numbers) == len(URLS)

URLS_base = ['www.google.com', 'www.example.com', 'www.python.org',
             'www.yahoo.com', 'www.ubc.ca', 'www.wikipedia.org']

for NUM in (5, 50, 500, 5000, 10000):
    URLS = []

    for _ in range(NUM):
        for url in URLS_base:
            URLS.append(url)

    print("--------------------")
    timeit(run_gevent_without_a_timeout, URLS)
    timeit(run_threads_correctly, URLS)

Berikut ini beberapa hasilnya:

--------------------
run_gevent_without_a_timeout / 30 hostnames, 0.044888 seconds
run_threads_correctly / 30 hostnames, 0.019389 seconds
--------------------
run_gevent_without_a_timeout / 300 hostnames, 0.186045 seconds
run_threads_correctly / 300 hostnames, 0.153808 seconds
--------------------
run_gevent_without_a_timeout / 3000 hostnames, 1.834089 seconds
run_threads_correctly / 3000 hostnames, 1.569523 seconds
--------------------
run_gevent_without_a_timeout / 30000 hostnames, 19.030259 seconds
run_threads_correctly / 30000 hostnames, 15.163603 seconds
--------------------
run_gevent_without_a_timeout / 60000 hostnames, 35.770358 seconds
run_threads_correctly / 60000 hostnames, 29.864083 seconds

kesalahpahaman yang dimiliki setiap orang tentang non-pemblokiran IO dengan Python adalah keyakinan bahwa juru bahasa Python dapat menangani pekerjaan mengambil hasil dari soket pada skala besar lebih cepat daripada koneksi jaringan itu sendiri dapat mengembalikan IO. Walaupun ini memang benar dalam beberapa kasus, itu tidak benar hampir sesering yang dipikirkan orang, karena interpreter Python benar-benar lambat. Dalam posting blog saya di sini , saya mengilustrasikan beberapa profil grafis yang menunjukkan bahwa untuk hal-hal yang sangat sederhana sekalipun, jika Anda berurusan dengan akses jaringan yang tajam dan cepat ke hal-hal seperti database atau server DNS, layanan tersebut dapat kembali jauh lebih cepat daripada kode Python dapat menangani ribuan koneksi tersebut.

zzzeek
sumber
8

Ini cukup menarik untuk dianalisis. Berikut ini adalah kode untuk membandingkan kinerja greenlets versus kumpulan multi-pemrosesan versus multi-threading:

import gevent
from gevent import socket as gsock
import socket as sock
from multiprocessing import Pool
from threading import Thread
from datetime import datetime

class IpGetter(Thread):
    def __init__(self, domain):
        Thread.__init__(self)
        self.domain = domain
    def run(self):
        self.ip = sock.gethostbyname(self.domain)

if __name__ == "__main__":
    URLS = ['www.google.com', 'www.example.com', 'www.python.org', 'www.yahoo.com', 'www.ubc.ca', 'www.wikipedia.org']
    t1 = datetime.now()
    jobs = [gevent.spawn(gsock.gethostbyname, url) for url in URLS]
    gevent.joinall(jobs, timeout=2)
    t2 = datetime.now()
    print "Using gevent it took: %s" % (t2-t1).total_seconds()
    print "-----------"
    t1 = datetime.now()
    pool = Pool(len(URLS))
    results = pool.map(sock.gethostbyname, URLS)
    t2 = datetime.now()
    pool.close()
    print "Using multiprocessing it took: %s" % (t2-t1).total_seconds()
    print "-----------"
    t1 = datetime.now()
    threads = []
    for url in URLS:
        t = IpGetter(url)
        t.start()
        threads.append(t)
    for t in threads:
        t.join()
    t2 = datetime.now()
    print "Using multi-threading it took: %s" % (t2-t1).total_seconds()

berikut hasilnya:

Using gevent it took: 0.083758
-----------
Using multiprocessing it took: 0.023633
-----------
Using multi-threading it took: 0.008327

Saya pikir Greenlet mengklaim bahwa itu tidak terikat oleh GIL tidak seperti perpustakaan multithreading. Selain itu, Greenlet doc mengatakan bahwa ini dimaksudkan untuk operasi jaringan. Untuk operasi jaringan yang intensif, penggantian ulir baik-baik saja dan Anda dapat melihat bahwa pendekatan multithreading cukup cepat. Juga selalu disukai menggunakan perpustakaan resmi python; Saya mencoba menginstal greenlet di windows dan mengalami masalah ketergantungan dll jadi saya menjalankan tes ini pada vm linux. Alway mencoba menulis kode dengan harapan dapat berjalan pada mesin apa pun.

maks
sumber
25
Perhatikan bahwa getsockbynamecache hasil pada level OS (setidaknya pada mesin saya itu hasilnya). Ketika dipanggil pada DNS yang sebelumnya tidak dikenal atau kedaluwarsa, itu akan benar-benar melakukan permintaan jaringan, yang mungkin memakan waktu. Ketika dipanggil pada nama host yang baru saja diselesaikan, itu akan mengembalikan jawabannya jauh lebih cepat. Akibatnya, metodologi pengukuran Anda cacat di sini. Ini menjelaskan hasil aneh Anda - gevent tidak bisa benar-benar jauh lebih buruk daripada multithreading - keduanya tidak-benar-paralel di tingkat VM.
KT.
1
@KT. itu adalah poin yang sangat bagus. Anda perlu menjalankan tes itu berkali-kali dan mengambil sarana, mode, dan median untuk mendapatkan gambar yang bagus. Perhatikan juga bahwa jalur rute cache cache untuk protokol dan di mana mereka tidak cache jalur rute Anda bisa mendapatkan jeda yang berbeda dari lalu lintas jalur rute dns yang berbeda. Dan server dns sangat cache. Mungkin lebih baik untuk mengukur threading menggunakan time.clock () di mana siklus CPU digunakan daripada dipengaruhi oleh latensi atas perangkat keras jaringan. Ini bisa menghilangkan layanan OS lain yang menyelinap masuk dan menambahkan waktu dari pengukuran Anda.
DevPlayer
Oh dan Anda dapat menjalankan flush dns di tingkat OS antara tiga tes tapi sekali lagi itu hanya akan mengurangi data palsu dari caching dns lokal.
DevPlayer
Ya. Menjalankan versi yang sudah dibersihkan ini: paste.ubuntu.com/p/pg3KTzT2FG Saya mendapatkan waktu yang hampir sama persis ...using_gevent() 421.442985535ms using_multiprocessing() 394.540071487ms using_multithreading() 402.48298645ms
sehe
Saya pikir OSX melakukan cns dns tetapi di Linux itu bukan "default": stackoverflow.com/a/11021207/34549 , jadi ya, pada tingkat rendah greenlets konkurensi jauh lebih buruk karena overhead juru bahasa
zzzeek