150

我是 gevents 和 greenlets 的新手。我找到了一些关于如何使用它们的好文档,但没有一个给我关于如何以及何时使用 greenlets 的理由!

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

我不确定的是,如果它们基本上是协同程序,它们如何为我们提供并发性。

4

4 回答 4

219

Greenlets 提供并发性但提供并行性。并发是指代码可以独立于其他代码运行。并行性是同时执行并发代码。当用户空间中有很多工作要做时,并行性特别有用,这通常是 CPU 密集型的东西。并发对于分解问题非常有用,可以更轻松地并行调度和管理不同的部分。

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

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

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

于 2013-03-24T07:47:32.777 回答
19

更正上面@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_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

每个人对 Python 的非阻塞 IO 的误解是相信 Python 解释器可以比网络连接本身返回 IO 更快地处理从套接字检索结果的工作。虽然在某些情况下确实如此,但并不像人们想象的那么频繁,因为 Python 解释器真的非常非常慢。在我的博客文章中,我展示了一些图形配置文件,这些配置文件表明,即使是非常简单的事情,如果您正在处理对数据库或 DNS 服务器等事物的清晰快速的网络访问,这些服务可以比 Python 代码更快地返回可以处理成千上万个这样的连接。

于 2018-08-20T14:02:01.807 回答
16

采用@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

所以你可以看到使用 gevent 的 I/O 有一些显着的差异

于 2015-03-06T21:16:18.327 回答
7

这很有趣,可以分析。这是一个比较greenlets与多处理池与多线程的性能的代码:

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

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

于 2014-12-12T23:27:33.863 回答