格林莱特 VS 丝线

我是新来的。我找到了一些关于如何使用它们的很好的文档,但是没有一篇文档告诉我应该如何以及何时使用 greenlet!

  • 他们到底擅长什么?
  • 在代理服务器中使用它们是否是一个好主意?
  • 为什么不是线呢?

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

49107 次浏览

Greenlet 提供并发性,但是提供 没有并行性。并发是指代码可以独立于其他代码运行。并行是同时执行并发代码。当用户空间中有很多工作要做时,并行性特别有用,而这些工作通常都是 CPU 负担很重的。并发性有助于分解问题,使不同的部分能够更容易地并行调度和管理。

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

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

Greenlet 提供了并发性,原因如前所述。并发不是并行。通过隐藏事件注册并在通常会阻塞当前线程的调用上为您执行调度,像 gevent 这样的项目不需要更改异步 API 就可以公开这种并发性,并且系统的成本显著降低。

这很有趣,值得分析。 下面是比较 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()

结果如下:

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 上运行了这个测试。 总是尝试编写代码,希望它能在任何机器上运行。

接受@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 有一些显著的不同

正如上面@颞存在的回答,greenlet 并不比线程“快”,而且产生 60000根线来解决并发性问题是一种不正确的编程技术,一个小的线程池反而是合适的。这里有一个更合理的比较(从我的 Reddit 帖子回应人们引用这个职位)。

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 代码处理成千上万个这样的连接的速度快得多。