เทียบกับ Greenlet หัวข้อ


141

ฉันยังใหม่กับ gevents และ greenlets ฉันพบเอกสารที่ดีเกี่ยวกับวิธีการทำงานกับเอกสารเหล่านั้น แต่ไม่มีใครให้เหตุผลแก่ฉันเกี่ยวกับวิธีและเวลาที่ฉันควรใช้กรีนเล็ต!

  • พวกเขาเก่งเรื่องอะไรดี?
  • เป็นความคิดที่ดีที่จะใช้มันในพร็อกซีเซิร์ฟเวอร์หรือไม่?
  • ทำไมไม่หัวข้อ?

สิ่งที่ฉันไม่แน่ใจคือพวกเขาสามารถให้เราพร้อมกันได้อย่างไร


1
@Imran เกี่ยวกับ greenthreads ใน Java คำถามของฉันเกี่ยวกับกรีนเล็ตใน Python ฉันพลาดอะไรไปรึเปล่า ?
Rsh

ตอนนี้, เธรดในไพ ธ อนไม่ได้เกิดขึ้นพร้อมกันจริงๆเพราะการล็อคล่ามทั่วโลก ดังนั้นมันจะต้มลงเพื่อเปรียบเทียบค่าใช้จ่ายของทั้งสองโซลูชั่น แม้ว่าฉันจะเข้าใจว่ามีการใช้งานของหลามหลายแบบ แต่ก็อาจไม่เหมาะกับทุกคน
didierc

3
@didierc CPython (และ PyPy ณ ตอนนี้) จะไม่ตีความรหัส Python (ไบต์) ในแบบคู่ขนาน (นั่นคือจริงๆแล้วทางกายภาพในเวลาเดียวกันในสองแกน CPU ที่แตกต่างกัน) อย่างไรก็ตามไม่ใช่ทุกโปรแกรมของ Python ที่อยู่ภายใต้ GIL (ตัวอย่างทั่วไปคือ syscalls รวมถึงฟังก์ชั่น I / O และ C ที่จงใจปล่อย GIL) และ a threading.Threadเป็นเธรด OS ที่มี ramifications ทั้งหมด ดังนั้นมันจึงไม่ง่ายอย่างนั้น อย่างไรก็ตาม Jython ไม่มี GIL AFAIK และ PyPy ที่พยายามจะกำจัดมันเช่นกัน

คำตอบ:


204

กรีนเล็ตจัดเตรียมภาวะพร้อมกัน แต่ไม่ขนานกัน การเกิดขึ้นพร้อมกันคือเมื่อรหัสสามารถทำงานโดยอิสระจากรหัสอื่น Parallelism เป็นการเรียกใช้โค้ดที่เกิดขึ้นพร้อมกัน การขนานนั้นมีประโยชน์อย่างยิ่งเมื่อมีงานจำนวนมากที่ต้องทำในพื้นที่ผู้ใช้และโดยทั่วไปแล้วสิ่งที่หนักหน่วงของ CPU การเห็นพ้องด้วยกันมีประโยชน์สำหรับการแยกปัญหาทำให้ส่วนต่าง ๆ สามารถกำหนดเวลาและจัดการได้ง่ายขึ้นในแบบคู่ขนาน

กรีนเล็ตส่องแสงจริง ๆ ในการเขียนโปรแกรมเครือข่ายซึ่งการโต้ตอบกับซ็อกเก็ตหนึ่งสามารถเกิดขึ้นได้โดยไม่เกี่ยวข้องกับซ็อกเก็ตอื่น นี่คือตัวอย่างคลาสสิกของการเกิดพร้อมกัน เนื่องจากแต่ละกรีนเล็ตทำงานในบริบทของตนเองคุณสามารถใช้ API แบบซิงโครนัสได้โดยไม่ต้องทำเกลียว สิ่งนี้เป็นสิ่งที่ดีเนื่องจากเธรดมีราคาแพงมากในแง่ของหน่วยความจำเสมือนและโอเวอร์เฮดของเคอร์เนลดังนั้นการทำงานพร้อมกันที่คุณสามารถทำได้ด้วยเธรดนั้นน้อยกว่ามาก นอกจากนี้การทำเกลียวใน Python นั้นแพงกว่าและ จำกัด กว่าปกติเนื่องจาก GIL ทางเลือกในการทำงานพร้อมกันมักเป็นโครงการเช่น Twisted, libevent, libuv, node.js ฯลฯ โดยที่รหัสทั้งหมดของคุณใช้บริบทการดำเนินการเดียวกันและลงทะเบียนตัวจัดการเหตุการณ์

เป็นความคิดที่ดีในการใช้กรีนเล็ต (ด้วยการสนับสนุนเครือข่ายที่เหมาะสมเช่นผ่าน gevent) สำหรับการเขียนพร็อกซีเนื่องจากการจัดการคำขอของคุณสามารถดำเนินการได้อย่างอิสระและควรเขียนเช่นนี้

กรีนเล็ตให้การเห็นพ้องด้วยเหตุผลที่ฉันให้ไว้ก่อนหน้านี้ การเกิดขึ้นพร้อมกันนั้นไม่ขนานกัน ด้วยการปกปิดการลงทะเบียนเหตุการณ์และดำเนินการตั้งเวลาให้กับคุณเมื่อมีการโทรซึ่งปกติจะปิดกั้นเธรดปัจจุบันโครงการอย่าง gevent จะเปิดเผยการทำงานพร้อมกันนี้โดยไม่ต้องเปลี่ยน API แบบอะซิงโครนัสและมีค่าใช้จ่ายน้อยลง


1
ขอขอบคุณเพียงคำถามสองข้อ: 1) เป็นไปได้หรือไม่ที่จะรวมโซลูชันนี้เข้ากับการประมวลผลหลายตัวเพื่อให้ได้ปริมาณงานที่สูงขึ้น? 2) ฉันยังไม่รู้ว่าทำไมถึงใช้เธรด เราสามารถพิจารณาพวกเขาว่าเป็นการใช้งานพร้อมกันขั้นพื้นฐานของการทำงานพร้อมกันในไลบรารีมาตรฐานหลามหรือไม่?
Rsh

6
1) ใช่แน่นอน คุณไม่ควรทำสิ่งนี้ก่อนเวลาอันควร แต่เนื่องจากปัจจัยหลายอย่างที่อยู่นอกเหนือขอบเขตของคำถามนี้การมีหลายกระบวนการที่ทำหน้าที่ร้องขอจะทำให้ปริมาณงานสูงขึ้น 2) ระบบปฏิบัติการเธรดมีการกำหนดเวลาไว้ล่วงหน้าและขนานกันอย่างเต็มที่ตามค่าเริ่มต้น สิ่งเหล่านี้เป็นค่าเริ่มต้นใน Python เนื่องจาก Python จะเปิดเผยอินเตอร์เฟสเธรดดั้งเดิมและเธรดเป็นส่วนที่ดีที่สุดที่สนับสนุนและเป็นตัวหารร่วมที่ต่ำที่สุดสำหรับทั้งคู่ขนานและการทำงานพร้อมกันในระบบปฏิบัติการสมัยใหม่
Matt Joiner

6
ฉันควรพูดถึงว่าคุณไม่ควรใช้กรีนเล็ตจนกว่ากระทู้จะไม่เป็นที่พอใจ (โดยปกติจะเกิดขึ้นเนื่องจากจำนวนการเชื่อมต่อพร้อมกันที่คุณจัดการอยู่และจำนวนเธรดหรือ GIL จะทำให้คุณเศร้าโศก) และแม้แต่ ถ้าไม่มีตัวเลือกอื่นให้คุณเท่านั้น ไลบรารีมาตรฐานของ Python และไลบรารี่ของบุคคลที่สามส่วนใหญ่คาดว่าจะเกิดขึ้นพร้อมกันผ่านทางเธรดดังนั้นคุณอาจได้รับพฤติกรรมแปลก ๆ ถ้าคุณให้ผ่านกรีนเล็ต
Matt Joiner

@ MattJoiner ฉันมีฟังก์ชั่นด้านล่างซึ่งอ่านไฟล์ขนาดใหญ่เพื่อคำนวณผลรวม md5 ฉันจะใช้ gevent ในกรณีนี้เพื่ออ่านได้เร็วขึ้นอย่างไร 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

รับคำตอบของ @ Max และเพิ่มความเกี่ยวข้องให้กับมาตราส่วนคุณสามารถเห็นความแตกต่าง ฉันได้รับสิ่งนี้โดยการเปลี่ยน URL ให้เป็นดังนี้:

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)

ฉันต้องเลื่อนรุ่นมัลติโพรเซสซิงออกก่อนที่ฉันจะมี 500; แต่ที่ 10,000 ซ้ำ:

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

ดังนั้นคุณจะเห็นว่ามีความแตกต่างอย่างมีนัยสำคัญใน I / O โดยใช้ gevent


4
มันไม่ถูกต้องอย่างสิ้นเชิงที่จะวางไข่ 60000 เธรดหรือกระบวนการเพื่อให้งานเสร็จสมบูรณ์และการทดสอบนี้แสดงให้เห็นว่าไม่มีอะไรเลย (เช่นกัน ลองใช้กลุ่มเธรดประมาณ 50 เธรดดูคำตอบของฉัน: stackoverflow.com/a/51932442/34549
zzzeek

9

การแก้ไขคำตอบของ @TemporalBeing ด้านบนกรีนเล็ตไม่ได้ "เร็ว" กว่าเธรดและเป็นเทคนิคการเขียนโปรแกรมที่ไม่ถูกต้องในการวางเธรด 60000 เธรดเพื่อแก้ไขปัญหาการทำงานพร้อมกัน นี่คือการเปรียบเทียบที่สมเหตุสมผลมากขึ้น (จากโพสต์ redditของฉันเพื่อตอบสนองต่อคนที่อ้างถึงโพสต์ SO นี้)

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)

นี่คือผลลัพธ์บางส่วน:

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

ความเข้าใจผิดที่ทุกคนมีเกี่ยวกับการไม่ปิดกั้น IO ด้วย Python คือความเชื่อที่ว่า Python interpreter สามารถเข้าร่วมในการดึงผลลัพธ์จากซ็อกเก็ตในขนาดใหญ่ได้เร็วกว่าการเชื่อมต่อเครือข่าย แม้ว่านี่จะเป็นจริงในบางกรณี แต่ก็ไม่เป็นความจริงอย่างที่คนทั่วไปคิดเพราะล่าม Python ช้าจริงๆ ในบล็อกโพสต์ของฉันที่นี่ฉันแสดงโปรไฟล์กราฟิกบางอย่างที่แสดงว่าสำหรับสิ่งที่ง่ายมากถ้าคุณจัดการกับเครือข่ายที่คมชัดและรวดเร็วในการเข้าถึงสิ่งต่าง ๆ เช่นฐานข้อมูลหรือเซิร์ฟเวอร์ DNS บริการเหล่านั้นสามารถกลับมาเร็วกว่ารหัส Python สามารถเข้าร่วมหลายพันคนของการเชื่อมต่อเหล่านั้น


8

มันน่าสนใจพอที่จะวิเคราะห์ นี่คือรหัสเพื่อเปรียบเทียบประสิทธิภาพของกรีนเล็ตกับพูลมัลติโปรเซสเซอร์และมัลติเธรด:

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

นี่คือผลลัพธ์:

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

ฉันคิดว่ากรีนเล็ตอ้างว่ามันไม่ได้ผูกพันโดย GIL ซึ่งแตกต่างจากห้องสมุดหลายเธรด นอกจากนี้ Greenlet doc บอกว่ามันมีความหมายสำหรับการดำเนินงานเครือข่าย สำหรับการใช้งานเครือข่ายอย่างเข้มข้นการสลับเธรดทำได้ดีและคุณจะเห็นว่าวิธีการทำงานมัลติเธรดนั้นค่อนข้างเร็ว นอกจากนี้ยังเป็นสิ่งที่น่าตื่นเต้นที่จะใช้ห้องสมุดของไพ ธ อนอยู่เสมอ ฉันพยายามติดตั้ง Greenlet บน windows และพบปัญหาการพึ่งพา dll ดังนั้นฉันจึงทำการทดสอบบน linux vm พยายามเขียนโค้ดด้วยความหวังว่ามันจะรันบนเครื่องใด ๆ


25
โปรดทราบว่าgetsockbynameแคชผลลัพธ์ในระดับระบบปฏิบัติการ (อย่างน้อยก็ในเครื่องของฉัน) เมื่อเรียกใช้บน DNS ที่ไม่รู้จักหรือหมดอายุก่อนหน้านี้จริง ๆ แล้วมันจะทำการสืบค้นเครือข่ายซึ่งอาจใช้เวลาสักครู่ เมื่อเรียกใช้กับชื่อโฮสต์ที่เพิ่งได้รับการแก้ไขมันจะส่งคืนคำตอบเร็วกว่ามาก ดังนั้นวิธีการวัดของคุณมีข้อบกพร่องที่นี่ สิ่งนี้อธิบายถึงผลลัพธ์ที่แปลกประหลาดของคุณ - gevent ไม่สามารถแย่ไปกว่าการใช้มัลติเธรดอย่างมาก - ทั้งคู่ไม่ได้ขนานกันอย่างแท้จริงในระดับ VM
KT

1
@KT นั่นคือจุดที่ยอดเยี่ยม คุณจะต้องทำการทดสอบหลาย ๆ ครั้งและใช้วิธีการโหมดและค่ามัธยฐานเพื่อให้ได้ภาพที่ดี โปรดทราบว่าเราเตอร์จะทำเส้นทางของแคชสำหรับโปรโตคอลและที่ที่พวกเขาไม่ได้เก็บเส้นทางของเส้นทางไว้คุณอาจได้รับความล่าช้าที่แตกต่างจากทราฟฟิกของเส้นทางเส้นทาง dns ที่แตกต่างกัน และเซิร์ฟเวอร์ DNS แคอย่างมาก การวัดเธรดโดยใช้ time.clock () ควรใช้วงจร cpu แทนการแฝงตัวของฮาร์ดแวร์เครือข่าย สิ่งนี้สามารถกำจัดบริการระบบปฏิบัติการอื่น ๆ ที่แอบเข้าไปและเพิ่มเวลาจากการวัดของคุณ
DevPlayer

โอ้และคุณสามารถเรียกใช้ dns flush ที่ระดับ OS ระหว่างการทดสอบทั้งสาม แต่อีกครั้งที่จะลดเฉพาะข้อมูลเท็จจากการแคช DNS ท้องถิ่น
DevPlayer

ได้. เล่นรุ่นนี้ทำความสะอาด: paste.ubuntu.com/p/pg3KTzT2FGฉันได้รับครั้งสวยมากเหมือน-ish ...using_gevent() 421.442985535ms using_multiprocessing() 394.540071487ms using_multithreading() 402.48298645ms
sehe

ฉันคิดว่า OSX กำลังแคช dns แต่บน Linux ไม่ใช่สิ่งที่ "เป็นค่าเริ่มต้น": stackoverflow.com/a/11021207/34549ดังนั้นใช่ที่กรีนเล็ตพร้อมกันในระดับต่ำนั้นแย่กว่ามากเนื่องจากค่าใช้จ่ายล่าม
zzzeek
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.