格林莱特与 主题

Rsh*_*Rsh 130 python concurrency coroutine gevent greenlets

我是gevents和greenlets的新手.我找到了一些关于如何使用它们的好文档,但是没有一个能让我证明我应该如何以及何时使用greenlets!

  • 他们真正擅长什么?
  • 在代理服务器中使用它们是不是一个好主意?
  • 为什么不是线程?

我不确定的是,如果它们基本上是共同惯例,它们如何为我们提供并发性.

Mat*_*ner 188

Greenlets提供并发性但提供并行性.并发是指代码可以独立于其他代码运行.并行性是同时执行并发代码.当在用户空间中进行大量工作时,并行性特别有用,而且通常是CPU占用大量的东西.并发对于分解问题很有用,可以更容易地并行地调度和管理不同的部分.

Greenlets确实在网络编程中发挥作用,其中与一个套接字的交互可以独立于与其他套接字的交互而发生.这是并发的典型示例.由于每个greenlet都在其自己的上下文中运行,因此您可以继续使用同步API而无需进行线程化.这很好,因为线程在虚拟内存和内核开销方面非常昂贵,因此使用线程实现的并发性要小得多.此外,由于GIL,Python中的线程比平时更昂贵且更有限.并发的替代方案通常是像Twisted,libevent,libuv,node.js等项目,其中所有代码共享相同的执行上下文,并注册事件处理程序.

使用greenlets(通过gevent提供适当的网络支持)来编写代理是一个很好的主意,因为您对请求的处理能够独立执行,并且应该这样写.

Greenlets提供了我之前给出的原因的并发性.并发不是并行性.通过隐藏事件注册并在通常会阻塞当前线程的调用上为您执行调度,像gevent这样的项目会暴露这种并发性,而无需更改异步API,并且系统的成本要低得多.

  • 1)是的,绝对的.您不应该过早地这样做,但由于超出此问题范围的一大堆因素,让多个进程服务请求将为您提供更高的吞吐量.2)OS线程被抢占式调度,默认情况下完全并行化.它们是Python中的默认值,因为Python公开了本机线程接口,并且线程是现代操作系统中并行性和并发性的最佳支持和最低公分母. (6认同)
  • 我应该提到你甚至不应该使用greenlets直到线程不满意(通常这是因为你正在处理的同时连接的数量,并且线程数或GIL让你感到悲伤),甚至那么只有当你没有其他选择时.Python标准库和大多数第三方库*期望通过线程实现并发性,因此如果通过greenlets提供,则可能会出现奇怪的行为. (6认同)

小智 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)
Run Code Online (Sandbox Code Playgroud)

我不得不退出多进程版本,因为它在我有500之前掉了下来; 但在10,000次迭代:

Using gevent it took: 3.756914
-----------
Using multi-threading it took: 15.797028
Run Code Online (Sandbox Code Playgroud)

因此,您可以看到使用gevent在I/O方面存在一些显着差异

  • 生成60000本机线程或进程来完成工作完全不正确,这个测试什么也没有显示(你也从gevent.joinall()调用中取消了超时?).尝试使用大约50个线程的线程池,请参阅我的回答:/sf/answers/3635270971/ (4认同)

max*_*max 8

这很有趣,可以分析.下面是一个代码来比较greenlet与多处理池和多线程的性能:

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()
Run Code Online (Sandbox Code Playgroud)

结果如下:

Using gevent it took: 0.083758
-----------
Using multiprocessing it took: 0.023633
-----------
Using multi-threading it took: 0.008327
Run Code Online (Sandbox Code Playgroud)

我认为greenlet声称它不受GIL的约束,不像多线程库.此外,Greenlet doc说它适用于网络运营.对于网络密集型操作,线程切换很好,您可以看到多线程方法非常快.使用python的官方库也总是无法实现的; 我尝试在Windows上安装greenlet并遇到dll依赖问题所以我在linux vm上运行了这个测试.总是尝试编写代码,希望它能在任何机器上运行.

  • 请注意,``getsockbyname``将结果缓存在操作系统级别(至少在我的机器上).在以前未知或过期的DNS上调用时,它实际上将执行网络查询,这可能需要一些时间.在刚刚解析的主机名上调用时,它将更快地返回答案.因此,您的测量方法存在缺陷.这解释了你的奇怪结果 - gevent实际上并不比多线程更糟糕 - 两者在VM级别都不是真正并行的. (24认同)

zzz*_*eek 5

纠正上述@TemporalBeing的答案,greenlets的速度并不比线程“快”,并且产生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 Code Online (Sandbox Code Playgroud)

结果如下:

--------------------
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
Run Code Online (Sandbox Code Playgroud)

每个人对使用Python进行非阻塞IO的误解都在于,人们认为Python解释器可以比网络连接本身返回IO更快地大规模地从套接字检索结果。尽管在某些情况下这确实是正确的,但事实并非如人们想象的那么频繁,因为Python解释器确实非常缓慢。在我的博客文章中,我说明了一些图形配置文件,这些配置文件显示即使是非常简单的事情,如果您要处理对数据库或DNS服务器等事物的快速便捷的网络访问,这些服务的返回速度都将比Python代码快得多。可以参加成千上万的此类联系。