Python中多线程编程的优点是什么?

时间:2012-10-21 08:39:24

标签: python multithreading eventlet

当我听说多线程编程时,我认为有机会加速我的程序,但事实并非如此?

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的一些特殊内容以及它们为什么不加速任务?

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

6 个答案:

答案 0 :(得分:6)

Cpython主题:

  • 每个cpython线程映射到OS级别线程(用户空间中的轻量级进程/ pthread)

  • 如果有许多cpython线程同时执行python代码:由于全局解释器锁定,只有一个cpython线程可以一次解释python。当需要解释python指令时,剩余的线程将在GIL上被阻塞。当有许多python线程时,这会减慢很多东西。

  • 现在,如果您的python代码大部分时间都花在网络操作中(发送,连接等):在这种情况下,将会有更少的线程争夺GIL来解释代码。所以GIL的效果并不是那么糟糕。

Eventlet / Green threads:

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

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

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

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

<强>摘要

对于上面的程序,“true”并发性比iflet模型(在1个处理器上运行的单个线程)更快(cpython版本,在多个处理器上运行5个线程)。

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

答案 1 :(得分:3)

您的问题的标题是“Python中多线程编程的优势是什么?”所以我给你一个例子,而不是试图解决你的问题。我在2005年购买的奔腾核心二人组上运行了一个python程序,运行windows xp,从finance.yahoo.com下载500个csv文件,每个大约2K字节,一个用于S&amp; P 500中的每个库存。它使用urllib2。如果我不使用线程,它需要2分钟,使用标准的python线程(40个线程),它在3到4秒之间,平均每个约1/4秒(这是挂钟时间,包括计算和I / O) )。当我查看每个线程(挂钟)的开始和停止时间时,存在巨大的重叠。我有一个像java程序一样运行的东西,python和java之间的性能几乎相同。与使用curllib的c ++相同,但curllib比java或python慢​​一点。我使用标准的python版本2.2.6

答案 2 :(得分:2)

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

如果你正在使用像cython这样的东西,那么C部分可以同时执行,这就是你看到加速的原因。

在纯Python程序中,没有性能优势(就可以完成的计算量而言),但它有时是编写执行大量IO的代码的最简单方法(例如,离开等待套接字读取的线程在你做其他事情时完成。)

答案 3 :(得分:1)

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

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

答案 4 :(得分:1)

使用threadingmultiprocessing模块可以使用现代CPU中普遍存在的多个核心。

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

因此标准cpython在使用线程时有一个重要的limitation:一次只有一个线程可以执行python字节码。

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

答案 5 :(得分:1)

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

  1. 如果您的系统具有多个CPU或核心,则可以让所有CPU同时执行所有应用程序代码。因此,例如,如果您的系统具有四个CPU,那么使用多线程处理的进程可能会快4倍(尽管在大多数情况下它的可能性很小,因为典型应用程序需要线程将其访问权限同步到共享资源,创建争用)。

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

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

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

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

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

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

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

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

      我希望这有帮助!