GreenletVS。线程数


141

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

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

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


1
@Imran关于Java中的greenthreads。我的问题是关于Python中的greenlet。我想念什么吗?
Rsh 2013年

Afaik,由于全局解释器锁定,Python中的线程实际上并不是真正并发的。因此,归结为比较这两种解决方案的开销。尽管我了解python有几种实现,所以这可能不适用于所有这些实现。
didierc

3
@didierc CPython(和现在的PyPy)不会并行解释Python(字节)代码(也就是说,实际上在两个不同的CPU内核上同时物理地)。但是,并非Python程序所做的一切都在GIL下(常见示例是系统调用,包括故意释放GIL的I / O和C函数),并且a threading.Thread实际上是具有所有分支的OS线程。因此,实际上并不是那么简单。顺便说一句,Jython没有GIL AFAIK,PyPy也试图摆脱它。

Answers:


204

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

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

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

出于我之前提到的原因,Greenlets提供了并发性。并发不是并行性。通过隐藏事件注册并为通常会阻塞当前线程的调用执行调度,gevent之类的项目无需更改异步API即可公开此并发性,而系统的成本却大大降低。


1
谢谢,只有两个小问题:1)是否可以将此解决方案与多处理程序结合起来以实现更高的吞吐量?2)我仍然不知道为什么要使用线程?我们是否可以将它们视为python标准库中并发的幼稚和基本实现?
2013年

6
1)是的,绝对如此。您不应该过早地执行此操作,但是由于超出了此问题范围的许多因素,具有多个进程来处理请求将为您提供更高的吞吐量。2)操作系统线程被抢先调度,并且在默认情况下完全并行化。它们是Python中的默认值,因为Python公开了本机线程接口,而线程是现代操作系统中并行性和并发性的最佳支持和最低通用性。
马特·乔纳

6
我应该提到的是,您甚至在线程不满意之前都不应该使用greenlet(通常是因为您正在处理的同时连接数增多,并且线程数或GIL都使您感到悲伤),甚至那么只有在没有其他选择可用时。Python标准库和大多数第三方库都期望通过线程来实现并发,因此,如果通过greenlets提供并发功能,则可能会出现奇怪的行为。
马特·乔纳

@MattJoiner我有以下功能,该功能读取巨大的文件以计算md5和。在这种情况下,我如何使用gevent来阅读更快 import hashlib def checksum_md5(filename): md5 = hashlib.md5() with open(filename,'rb') as f: for chunk in iter(lambda: f.read(8192), b''): md5.update(chunk) return md5.digest()
Soumya

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)

在我有500个版本之前,我不得不放弃多进程版本。但经过10,000次迭代:

Using gevent it took: 3.756914
-----------
Using multi-threading it took: 15.797028

因此,您可以看到使用gevent的I / O有一些明显的不同


4
生成60000个本机线程或进程来完成工作是完全不正确的,并且此测试什么也没显示(您是否还从gevent.joinall()调用中取消了超时?)。尝试使用大约50个线程的线程池,请参阅我的答案:stackoverflow.com/a/51932442/34549
zzzeek

9

纠正上面的@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代码快得多。可以参加成千上万的此类联系。


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()

结果如下:

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上运行了该测试。始终尝试编写代码,希望它可以在任何计算机上运行。


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

1
@KT。这是一个很好的观点。您需要多次运行该测试,并采用均值,众数和中位数来获得良好的效果。还要注意,路由器会为协议缓存路由路径,而在没有缓存路由路径的地方,您可能会从不同的dns路由路径流量中得到不同的滞后。dns服务器大量缓存。最好使用time.clock()测量线程,其中使用cpu周期而不是受网络硬件上的延迟影响。这样可以消除其他OS服务潜行并增加测量时间的麻烦。
DevPlayer

哦,您可以在这三个测试之间的操作系统级别上运行dns刷新,但是再次这样做只能减少本地dns缓存中的错误数据。
DevPlayer

对。运行该清理版本:paste.ubuntu.com/p/pg3KTzT2FG我得到几乎相同的上下的时间...using_gevent() 421.442985535ms using_multiprocessing() 394.540071487ms using_multithreading() 402.48298645ms
sehe

我认为OSX正在进行dns缓存,但在Linux上这并不是“默认”的事情:stackoverflow.com/a/11021207/34549,所以,是的,由于解释器开销,并发级别较低的greenlets更加糟糕
zzzeek
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.