Greenlet与线程

157 投票
4 回答
52064 浏览
提问于 2025-04-17 19:55

我刚接触gevents和greenlets,发现了一些不错的文档来学习它们的用法,但没有人告诉我到底什么时候该用greenlets!

  • 它们到底擅长什么呢?
  • 在代理服务器中使用它们是个好主意吗?
  • 那为什么不直接用线程呢?

我不太明白的是,既然它们基本上是协程,那它们是怎么让我们实现并发的呢?

4 个回答

16

根据@Max的回答,我在这里加了一些关于扩展性的相关内容,你可以看到其中的区别。我通过改变网址的填写方式来实现这一点:

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在输入输出方面有明显的差别。

20

针对@TemporalBeing上面的回答,绿线程并不比线程“快”,用60000个线程来解决并发问题是一种错误的编程方式,实际上应该使用一个小的线程池。下面是一个更合理的比较(来自我在reddit上的帖子,是对引用这个StackOverflow帖子的人们的回应)。

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解释器能比网络连接本身更快地处理从套接字获取结果的工作。虽然在某些情况下确实如此,但并不是大家想象的那么频繁,因为Python解释器实际上非常慢。在我博客帖子中,我展示了一些图形化的分析,表明即使是非常简单的操作,如果你在处理快速的网络访问,比如数据库或DNS服务器,这些服务的响应速度往往比Python代码处理成千上万的连接要快得多。

225

Greenlets 提供了并发,但并不意味着并行。并发是指代码可以独立于其他代码运行,而并行则是指多个并发的代码同时执行。当需要处理大量用户空间的工作时,并行特别有用,这通常是一些需要大量 CPU 资源的任务。并发则有助于将问题拆分开来,使得不同的部分可以更容易地被安排和管理。

在网络编程中,Greenlets 的优势非常明显,因为与一个 socket 的交互可以独立于与其他 sockets 的交互。这就是并发的经典例子。由于每个 greenlet 都在自己的上下文中运行,你可以继续使用同步的 API,而不需要使用线程。这是好的,因为线程在虚拟内存和内核开销方面非常昂贵,所以通过线程实现的并发效果会大打折扣。此外,由于全局解释器锁(GIL)的存在,Python 中的线程使用成本更高,限制也更多。通常,替代并发的方案有 Twisted、libevent、libuv、node.js 等项目,这些项目中的所有代码共享同一个执行上下文,并注册事件处理程序。

使用 greenlets(配合适当的网络支持,比如 gevent)来编写代理是个很好的主意,因为你处理请求的方式可以独立执行,并且应该这样编写。

Greenlets 提供并发的原因就是我之前提到的。并发并不等于并行。通过隐藏事件注册并为你调度那些通常会阻塞当前线程的调用,像 gevent 这样的项目在不需要改变异步 API 的情况下,展现了这种并发,并且对你的系统的成本大大降低。

撰写回答