Greenlet Vs. Chủ đề


141

Tôi là người mới đến gevents và greenlets. Tôi tìm thấy một số tài liệu tốt về cách làm việc với họ, nhưng không có tài liệu nào cho tôi biết cách thức và thời điểm tôi nên sử dụng greenlets!

  • Họ thực sự giỏi về cái gì?
  • Có nên sử dụng chúng trong máy chủ proxy hay không?
  • Tại sao không phải chủ đề?

Điều tôi không chắc chắn là làm thế nào họ có thể cung cấp cho chúng tôi đồng thời nếu về cơ bản chúng là các thói quen chung.


1
@Imran Đó là về greenthreads trong Java. Câu hỏi của tôi là về greenlet trong Python. Tui bỏ lỡ điều gì vậy ?
Rsh

Afaik, chủ đề trong python thực sự không thực sự đồng thời vì khóa phiên dịch toàn cầu. Vì vậy, nó sẽ sôi sục để so sánh chi phí của cả hai giải pháp. Mặc dù tôi hiểu rằng có một số triển khai của python, vì vậy điều này có thể không áp dụng cho tất cả chúng.
didierc

3
@didierc CPython (và PyPy tính đến thời điểm hiện tại) sẽ không diễn giải mã Python (byte) song song (nghĩa là thực sự đồng thời về mặt vật lý trên hai lõi CPU riêng biệt). Tuy nhiên, không phải tất cả mọi thứ mà chương trình Python thực hiện đều nằm trong GIL (ví dụ phổ biến là các tòa nhà bao gồm các chức năng I / O và C cố tình giải phóng GIL) và threading.Threadthực sự là một luồng hệ điều hành với tất cả các phân nhánh. Vì vậy, nó thực sự không hoàn toàn đơn giản. Nhân tiện, Jython không có GIL AFAIK và PyPy cũng đang cố gắng loại bỏ nó.

Câu trả lời:


204

Greenlets cung cấp đồng thời nhưng không song song. Đồng thời là khi mã có thể chạy độc lập với mã khác. Song song là việc thực thi mã đồng thời. Tính song song đặc biệt hữu ích khi có rất nhiều việc phải làm trong không gian người dùng và đó thường là những thứ nặng về CPU. Đồng thời là hữu ích để phá vỡ các vấn đề, cho phép các phần khác nhau được lên lịch và quản lý song song dễ dàng hơn.

Greenlets thực sự tỏa sáng trong lập trình mạng nơi các tương tác với một ổ cắm có thể xảy ra độc lập với các tương tác với các ổ cắm khác. Đây là một ví dụ cổ điển về sự tương tranh. Vì mỗi greenlet chạy trong ngữ cảnh riêng của nó, bạn có thể tiếp tục sử dụng API đồng bộ mà không cần luồng. Điều này là tốt bởi vì các luồng rất tốn kém về bộ nhớ ảo và chi phí nhân, do đó tính đồng thời bạn có thể đạt được với các luồng ít hơn đáng kể. Ngoài ra, phân luồng trong Python đắt hơn và hạn chế hơn bình thường do GIL. Các lựa chọn thay thế cho đồng thời thường là các dự án như Twisted, libevent, libuv, node.js, v.v., trong đó tất cả các mã của bạn có chung bối cảnh thực thi và đăng ký xử lý sự kiện.

Đó là một ý tưởng tuyệt vời để sử dụng greenlets (với sự hỗ trợ mạng phù hợp như thông qua gevent) để viết proxy, vì việc xử lý các yêu cầu của bạn có thể thực thi độc lập và nên được viết như vậy.

Greenlets cung cấp đồng thời cho các lý do tôi đã đưa ra trước đó. Đồng thời không phải là song song. Bằng cách che giấu đăng ký sự kiện và thực hiện lập lịch cho bạn đối với các cuộc gọi thường chặn luồng hiện tại, các dự án như gevent sẽ phơi bày sự đồng thời này mà không yêu cầu thay đổi API không đồng bộ và với chi phí thấp hơn đáng kể cho hệ thống của bạn.


1
Cảm ơn, chỉ hai câu hỏi nhỏ: 1) Có thể kết hợp giải pháp này với đa xử lý để đạt được thông lượng cao hơn không? 2) Tôi vẫn không biết tại sao bao giờ sử dụng chủ đề? Chúng ta có thể coi chúng là một triển khai đồng thời ngây thơ và cơ bản trong thư viện chuẩn python không?
Rsh

6
1) Vâng, hoàn toàn. Bạn không nên thực hiện việc này sớm, nhưng vì một loạt các yếu tố nằm ngoài phạm vi của câu hỏi này, việc có nhiều quy trình phục vụ yêu cầu sẽ mang lại cho bạn thông lượng cao hơn. 2) Các luồng hệ điều hành được lên lịch trước và hoàn toàn song song theo mặc định. Chúng là mặc định trong Python vì Python hiển thị giao diện luồng gốc và các luồng là mẫu số chung được hỗ trợ tốt nhất và thấp nhất cho cả song song và đồng thời trong các hệ điều hành hiện đại.
Matt Tham gia

6
Tôi nên đề cập rằng bạn thậm chí không nên sử dụng greenlets cho đến khi các luồng không thỏa đáng (thường thì điều này xảy ra do số lượng kết nối đồng thời bạn đang xử lý và số lượng luồng hoặc GIL đang mang lại cho bạn sự đau buồn), và thậm chí sau đó chỉ khi không có một số tùy chọn khác có sẵn cho bạn. Thư viện chuẩn Python và hầu hết các thư viện bên thứ ba đều mong muốn đạt được sự đồng thời thông qua các luồng, do đó bạn có thể có hành vi lạ nếu bạn cung cấp thông qua greenlets.
Matt Tham gia

@MattJoiner Tôi có chức năng dưới đây đọc tệp lớn để tính tổng md5. Làm thế nào tôi có thể sử dụng gevent trong trường hợp này để đọc nhanh hơn 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

Lấy câu trả lời của @ Max và thêm một số mức độ liên quan đến nó để nhân rộng, bạn có thể thấy sự khác biệt. Tôi đã đạt được điều này bằng cách thay đổi các URL cần điền như sau:

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)

Tôi đã phải bỏ phiên bản đa xử lý vì nó đã giảm trước khi tôi có 500; nhưng ở 10.000 lần lặp:

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

Vì vậy, bạn có thể thấy có một số khác biệt đáng kể trong I / O bằng cách sử dụng gevent


4
Hoàn toàn không chính xác khi sinh ra 60000 luồng hoặc quy trình gốc để hoàn thành công việc và thử nghiệm này không cho thấy gì (bạn cũng đã hết thời gian chờ cuộc gọi gevent.joinall ()?). Hãy thử sử dụng nhóm chủ đề gồm khoảng 50 chủ đề, xem câu trả lời của tôi: stackoverflow.com/a/51932442/34549
zzzeek

9

Sửa lỗi cho câu trả lời của @TemporalBying ở trên, greenlets không "nhanh" hơn các luồng và đó là một kỹ thuật lập trình không chính xác để tạo ra 60000 luồng để giải quyết vấn đề tương tranh, thay vào đó là một nhóm các luồng nhỏ. Dưới đây là một so sánh hợp lý hơn (từ bài đăng trên reddit của tôi để phản hồi lại những người trích dẫn bài đăng SO này).

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)

Đây là một số kết quả:

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

sự hiểu lầm của mọi người về việc không chặn IO với Python là niềm tin rằng trình thông dịch Python có thể tham gia vào công việc truy xuất kết quả từ các socket ở quy mô lớn nhanh hơn chính các kết nối mạng có thể trả về IO. Mặc dù điều này chắc chắn đúng trong một số trường hợp, nhưng nó không đúng gần như mọi người nghĩ, bởi vì trình thông dịch Python thực sự rất chậm. Trong bài đăng trên blog của tôi ở đây , tôi minh họa một số cấu hình đồ họa cho thấy rằng ngay cả những điều rất đơn giản, nếu bạn đang xử lý truy cập mạng nhanh và sắc nét vào những thứ như cơ sở dữ liệu hoặc máy chủ DNS, các dịch vụ đó có thể quay lại nhanh hơn rất nhiều so với mã Python có thể tham dự đến hàng ngàn kết nối đó.


8

Điều này đủ thú vị để phân tích. Đây là một mã để so sánh hiệu suất của greenlets so với nhóm đa xử lý so với đa luồng:

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

đây là kết quả:

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

Tôi nghĩ rằng greenlet tuyên bố rằng nó không bị ràng buộc bởi GIL không giống như thư viện đa luồng. Hơn nữa, Greenlet doc nói rằng nó có nghĩa là cho các hoạt động mạng. Đối với một hoạt động chuyên sâu mạng, chuyển đổi luồng là tốt và bạn có thể thấy rằng phương pháp đa luồng là khá nhanh. Ngoài ra, luôn luôn có thể sử dụng các thư viện chính thức của python; Tôi đã thử cài đặt greenlet trên windows và gặp phải vấn đề phụ thuộc dll nên tôi đã chạy thử nghiệm này trên linux vm. Dù sao, hãy cố gắng viết một mã với hy vọng rằng nó chạy trên bất kỳ máy nào.


25
Lưu ý rằng getsockbynamelưu trữ kết quả ở cấp độ HĐH (ít nhất là trên máy của tôi). Khi được gọi trên một DNS chưa biết hoặc đã hết hạn trước đó, nó thực sự sẽ thực hiện một truy vấn mạng, có thể mất một thời gian. Khi được gọi trên một tên máy chủ vừa được giải quyết, nó sẽ trả lời nhanh hơn nhiều. Do đó, phương pháp đo lường của bạn là thiếu sót ở đây. Điều này giải thích kết quả kỳ lạ của bạn - gevent thực sự không thể tệ hơn nhiều so với đa luồng - cả hai đều không thực sự song song ở cấp VM.
KT.

1
@KT. Đó là điểm số tuyệt vời. Bạn sẽ cần phải chạy thử nghiệm đó nhiều lần và sử dụng các phương tiện, chế độ và dải phân cách để có được một bức ảnh đẹp. Cũng lưu ý rằng bộ định tuyến bộ đệm đường dẫn bộ đệm cho các giao thức và nơi chúng không lưu bộ đệm đường dẫn bộ đệm, bạn có thể nhận được độ trễ khác nhau từ lưu lượng đường dẫn tuyến đường dns khác nhau. Và máy chủ dns cache rất nhiều. Có thể tốt hơn để đo luồng bằng cách sử dụng time.clock () trong đó các chu kỳ cpu được sử dụng thay vì bị ảnh hưởng bởi độ trễ trên phần cứng mạng. Điều này có thể loại bỏ các dịch vụ HĐH khác lẻn vào và thêm thời gian từ các phép đo của bạn.
DevPlayer

Oh và bạn có thể chạy một dns tuôn ra ở cấp độ HĐH giữa ba thử nghiệm đó nhưng một lần nữa điều đó sẽ chỉ làm giảm dữ liệu sai từ bộ nhớ đệm dns cục bộ.
DevPlayer

Vâng Chạy phiên bản đã được dọn sạch này: paste.ubfox.com/p/pg3KTzT2FG Tôi nhận được khá nhiều lần giống nhau ...using_gevent() 421.442985535ms using_multiprocessing() 394.540071487ms using_multithreading() 402.48298645ms
sehe

Tôi nghĩ OSX đang thực hiện bộ nhớ đệm dns nhưng trên Linux thì đó không phải là điều "mặc định": stackoverflow.com/a/11021207/34549 , vì vậy, ở mức độ thấp của các đồng cỏ xanh đồng thời còn tệ hơn nhiều do chi phí phiên dịch viên
zzzeek
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.