我刚接触gevents和greenlets。 我找到了一些关于如何使用它们的好文档,但没有给出在什么情况下应该使用greenlets的理由!
- 它们真正擅长什么?
- 在代理服务器中使用它们是个好主意吗?
- 为什么不用线程?
我不确定的是,如果它们基本上是协程,它们如何提供并发。
我刚接触gevents和greenlets。 我找到了一些关于如何使用它们的好文档,但没有给出在什么情况下应该使用greenlets的理由!
我不确定的是,如果它们基本上是协程,它们如何提供并发。
Greenlets提供并发性但不提供并行性。并发是指代码可以独立于其他代码运行。并行是同时执行并发代码的过程。当用户空间中有大量工作需要完成时,特别是计算密集型任务,这种并行性尤为有用。并发性对于分解问题、使不同部分更容易地并行调度和管理非常有用。
Greenlets在网络编程中表现非常出色,其中与一个套接字的交互可以独立于与其他套接字的交互进行。这是并发的典型例子。由于每个greenlet都在自己的上下文中运行,因此您可以继续使用同步API而无需线程。这很好,因为线程在虚拟内存和内核开销方面非常昂贵,因此您可以使用线程实现的并发性显著较少。此外,由于全局解释器锁(GIL)的存在,Python中的线程比通常更昂贵和更受限制。替代并发性的项目通常是类似Twisted、libevent、libuv、node.js等,所有代码都共享相同的执行上下文,并注册事件处理程序。
对于编写代理,使用greenlets(通过gevent等适当的网络支持)是一个极好的选择,因为处理请求能够独立执行并应该按此方式编写。
Greenlets提供并发性,原因如前所述。并发不等于并行。通过在调用通常会阻塞当前线程的情况下隐藏事件注册并执行调度,像gevent这样的项目可以在不需要更改异步API的情况下暴露这种并发性,并且对您的系统造成的成本显著较少。
修正 @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
socket.connect
操作而不是仅仅将主机名解析为 IP 地址,使用 greenlet 代码速度更快(每秒约 10000 个套接字连接速度提高了 3 倍)。在使用 greenlets 时,我必须使用 gevent.pool.Pool(50)
来限制打开套接字的数量。我认为使用 socket.connect 进行测试是网络编程中使用 greenlets 更现实的用法,而不是仅仅解析主机名。 - smac89参考@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存在一些显著的差异
这很有趣,可以进行分析。 以下是一个代码示例,用于比较greenlets和多进程池(multiprocessing pool)以及多线程的性能:
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文档表示它是为网络操作而设计的。对于网络密集型操作,线程切换是可以接受的,你会发现多线程方法非常快。 此外,最好使用Python的官方库;我在Windows上尝试安装Greenlet时遇到了dll依赖问题,所以我在Linux虚拟机上运行了这个测试。 始终尝试编写可在任何计算机上运行的代码。
getsockbyname
会在操作系统级别缓存结果(至少在我的机器上是这样)。当对先前未知或已过期的 DNS 进行调用时,它将实际执行网络查询,这可能需要一些时间。当对刚刚解析的主机名进行调用时,它将更快地返回答案。因此,您的测量方法在这里存在缺陷。这就解释了您奇怪的结果 - gevent 实际上不可能比多线程差那么多 - 在 VM 级别上都不是真正的并行。 - KT.using_gevent() 421.442985535ms using_multiprocessing() 394.540071487ms using_multithreading() 402.48298645ms
- sehe
threading.Thread
实际上是具有所有影响的操作系统线程。因此,情况真的不是那么简单。顺便说一句,据我所知,Jython没有GIL,PyPy也试图摆脱它。 - user395760