3

当我听说多线程编程时,我想到了加速我的程序的机会,但不是吗?

import eventlet
from eventlet.green import socket
from iptools import IpRangeList


class Scanner(object):
    def __init__(self, ip_range, port_range, workers_num):
        self.workers_num = workers_num or 1000
        self.ip_range = self._get_ip_range(ip_range)
        self.port_range = self._get_port_range(port_range)
        self.scaned_range = self._get_scaned_range()

    def _get_ip_range(self, ip_range):
        return [ip for ip in IpRangeList(ip_range)]

    def _get_port_range(self, port_range):
        return [r for r in range(*port_range)]

    def _get_scaned_range(self):
        for ip in self.ip_range:
            for port in self.port_range:
                yield (ip, port)

    def scan(self, address):
        try:
            return bool(socket.create_connection(address))
        except:
            return False

    def run(self):
        pool = eventlet.GreenPool(self.workers_num)
        for status in pool.imap(self.scan, self.scaned_range):
            if status:
                yield True

    def run_std(self):
        for status in map(self.scan, self.scaned_range):
            if status:
                yield True


if __name__ == '__main__':
    s = Scanner(('127.0.0.1'), (1, 65000), 100000)
    import time
    now = time.time()
    open_ports = [i for i in s.run()]
    print 'Eventlet time: %s (sec) open: %s' % (now - time.time(),
                                                len(open_ports))
    del s
    s = Scanner(('127.0.0.1'), (1, 65000), 100000)
    now = time.time()
    open_ports = [i for i in s.run()]
    print 'CPython time: %s (sec) open: %s' % (now - time.time(),
                                                len(open_ports))

结果:

Eventlet time: -4.40343403816 (sec) open: 2
CPython time: -4.48356699944 (sec) open: 2

我的问题是,如果我不是在我的笔记本电脑上而是在服务器上运行这段代码并设置更多的工人价值,它会比 CPython 的版本运行得更快吗?线程的优点是什么?

添加: 所以我使用原始 cpython 的线程重写应用程序

import socket
from threading import Thread
from Queue import Queue

from iptools import IpRangeList

class Scanner(object):
    def __init__(self, ip_range, port_range, workers_num):
        self.workers_num = workers_num or 1000
        self.ip_range = self._get_ip_range(ip_range)
        self.port_range = self._get_port_range(port_range)
        self.scaned_range = [i for i in self._get_scaned_range()]

    def _get_ip_range(self, ip_range):
        return [ip for ip in IpRangeList(ip_range)]

    def _get_port_range(self, port_range):
        return [r for r in range(*port_range)]

    def _get_scaned_range(self):
        for ip in self.ip_range:
            for port in self.port_range:
                yield (ip, port)

    def scan(self, q):
        while True:
            try:
                r = bool(socket.create_conection(q.get()))
            except Exception:
                r = False
            q.task_done()

    def run(self):
        queue = Queue()
        for address in self.scaned_range:
                queue.put(address)
        for i in range(self.workers_num):
                worker = Thread(target=self.scan,args=(queue,))
                worker.setDaemon(True)
                worker.start()
        queue.join()


if __name__ == '__main__':
    s = Scanner(('127.0.0.1'), (1, 65000), 5)
    import time
    now = time.time()
    s.run()
    print time.time() - now

结果是

 Cpython's thread: 1.4 sec

我认为这是一个非常好的结果。我以标准的 nmap 扫描时间为标准:

$ nmap 127.0.0.1 -p1-65000

Starting Nmap 5.21 ( http://nmap.org ) at 2012-10-22 18:43 MSK
Nmap scan report for localhost (127.0.0.1)
Host is up (0.00021s latency).
Not shown: 64986 closed ports
PORT      STATE SERVICE
53/tcp    open  domain
80/tcp    open  http
443/tcp   open  https
631/tcp   open  ipp
3306/tcp  open  mysql
6379/tcp  open  unknown
8000/tcp  open  http-alt
8020/tcp  open  unknown
8888/tcp  open  sun-answerbook
9980/tcp  open  unknown
27017/tcp open  unknown
27634/tcp open  unknown
28017/tcp open  unknown
39900/tcp open  unknown

Nmap done: 1 IP address (1 host up) scanned in 0.85 seconds

我现在的问题是:据我所知,Eventlet 中的线程是如何实现的,这不是线程,而是 Eventlet 的特别之处,为什么它们不能加速任务?

Eventlet 被 OpenStack 等许多主要项目使用。但为什么呢?只是以异步方式或其他方式对数据库进行繁重的查询?

4

6 回答 6

6

Cpython线程:

  • 每个 cpython 线程都映射到一个操作系统级别的线程(用户空间中的轻量级进程/pthread)

  • 如果有多个cpython线程同时执行python代码:由于全局解释器锁,一次只能有一个cpython线程解释python。当需要解释 python 指令时,其余线程将在 GIL 上被阻塞。当有许多 python 线程时,这会大大降低速度。

  • 现在,如果您的 python 代码大部分时间都花在网络操作(发送、连接等)中:在这种情况下,争夺 GIL 解释代码的线程将会减少。所以GIL的效果还不错。

Eventlet/绿色线程:

  • 从上面我们知道cpython对线程有性能限制。Eventlets 尝试通过使用在单核上运行的单线程并为所有事情使用非阻塞 i/o 来解决问题。

  • 绿色线程不是真正的操作系统级线程。它们是并发的用户空间抽象。最重要的是,N 个绿色线程将映射到 1 个 OS 线程。这避免了 GIL 问题。

  • 绿色线程相互合作让步,而不是抢先调度。对于网络操作,套接字库在运行时进行修补(猴子修补),以便所有调用都是非阻塞的。

  • 因此,即使您创建了一个 eventlet 绿色线程池,您实际上也只创建了一个操作系统级别的线程。这个单一的操作系统级线程将执行所有的小事件。这个想法是,如果所有网络调用都是非阻塞的,那么在某些情况下,这应该比 python 线程更快。

概括

对于您上面的程序,“真正的”并发恰好比 eventlet 模型(在 1 个处理器上运行的单线程)更快(cpython 版本,5 个线程在多个处理器上运行)。

有一些 cpython 工作负载会在许多线程/内核上表现不佳(例如,如果您有 100 个客户端连接到服务器,每个客户端一个线程)。Eventlet 是用于此类工作负载的优雅编程模型,因此它在多个地方使用。

于 2012-10-24T03:37:12.867 回答
3

您的问题的标题是“Python 中多线程编程的优势是什么?” 所以我给你一个例子,而不是试图解决你的问题。我有一个 python 程序在我 2005 年购买的 pentium core duo 上运行,运行 windows xp,从 Finance.yahoo.com 下载 500 个 csv 文件,每个大约 2K 字节,一个用于标准普尔 500 指数中的每只股票。它使用 urllib2 . 如果我不使用线程,则需要超过 2 分钟,使用标准 python 线程(40 个线程)它在 3 到 4 秒之间,平均每个大约 1/4 秒(这是挂钟时间,包括计算和 I/O )。当我查看每个线程(挂钟)的开始和停止时间时,会有很大的重叠。我有同样的东西作为java程序运行,python和java之间的性能几乎相同。也与使用 curllib 的 c++ 相同,但 curllib 比 java 或 python 慢一点。我正在使用标准 python 版本 2.2.6

于 2012-10-24T04:06:16.650 回答
2

Python 有一个全局解释器锁http://en.wikipedia.org/wiki/Global_Interpreter_Lock可以防止两个线程同时执行。

如果您使用的是 cython 之类的东西,C 部分可以同时执行,这就是您看到加速的原因。

在纯 python 程序中没有性能优势(就您可以完成的计算量而言),但有时它是编写执行大量 IO 的代码的最简单方法(例如,当您等待套接字读取完成时,让线程等待做点别的)。

于 2012-10-24T03:08:10.007 回答
1

使用threadingormultiprocessing模块使您能够使用现代 CPU 中普遍存在的多个内核。

这是有代价的;增加了程序的复杂性,需要规范对共享数据的访问(尤其是写入);如果一个线程正在迭代一个列表,而另一个线程正在更新它,那么结果将是不确定的。这也适用于 python 解释器的内部数据。

因此,标准 cpython 在使用线程方面有一个重要限制:一次只能有一个线程执行 python 字节码。

如果您想并行化一个不需要在实例之间进行大量通信的作业,multiprocessing(尤其是multiprocessing.Pool)通常比线程更好,因为这些作业在不相互影响的不同进程中运行。

于 2012-10-23T11:08:57.097 回答
1

无论编程语言如何,多线程编程的主要优点是:

  1. 如果您的系统具有多个 CPU 或内核,那么您可以让所有 CPU 同时执行应用程序代码。例如,如果您有一个具有四个 CPU 的系统,则一个进程可能会通过多线程运行速度提高 4 倍(尽管在大多数情况下它不太可能这么快,因为典型的应用程序需要线程来同步它们对共享资源,创造竞争)。

  2. 如果进程由于某种原因需要阻塞(磁盘 I/O、用户输入、网络 I/O),那么当一个或多个线程被阻塞等待 I/O 完成时,其他线程可以做其他工作。请注意,对于这种类型的并发,您不需要多个 CPU 或内核,在单个 CPU 上运行的进程也可以从线程中受益匪浅。

这些好处是否可以应用于您的流程将在很大程度上取决于您的流程所做的事情。在某些情况下,您将获得相当大的性能改进,而在其他情况下,您不会,并且线程版本可能会更慢。请注意,编写良好且高效的多线程应用程序很困难。

现在,由于您特别询问 Python,让我们讨论这些好处如何应用于 Python。

  1. 由于 Python 中存在全局解释器锁,因此无法在多个 CPU 中并行运行代码。GIL 确保一次只有一个线程在解释 Python 代码,因此实际上没有办法充分利用多个 CPU。

  2. 如果一个 Python 线程执行阻塞操作,另一个线程将获得 CPU 并继续运行,而第一个线程被阻塞等待。当阻塞事件完成时,被阻塞的线程将恢复。所以这是在 Python 脚本中实现多线程的一个很好的理由(尽管它不是实现这种并发的唯一方法,非阻塞 I/O 可以实现类似的结果)。

以下是一些受益于使用多线程的示例:

  • 执行长时间操作的 GUI 程序可以有一个线程继续保持应用程序窗口刷新和响应,甚至可能显示关于长时间操作的进度报告和取消按钮。

  • 需要重复从磁盘读取记录,然后对它们进行一些处理并最终将它们写回磁盘的进程可以从线程中受益,因为当一个线程被阻塞等待从磁盘获取记录时,另一个线程可以处理另一条记录已被读取,但另一个线程可以将另一条记录写回磁盘。当进程读取或写入磁盘时没有线程,其他任何事情都不会发生。对于没有 GIL(比如 C++)的语言,好处更大,因为您还可以有多个线程,每个线程都运行在不同的内核上,都在处理不同的记录。

我希望这有帮助!

于 2012-10-24T06:08:44.960 回答
0

添加线程不一定会使进程更快,因为与线程管理相关的开销可能超过您从线程获得的任何性能增益。

如果您在一台 CPU 较少而不是 CPU 较多的机器上运行此程序,您可能会发现它运行速度较慢,因为它将每个线程交换进出执行。可能还有其他因素在起作用。如果线程需要访问无法处理并发请求的其他子系统或硬件(例如串行端口),那么多线程不会帮助您提高性能。

于 2012-10-21T08:46:06.047 回答