Python中多线程编程有什么优势?

4

当我听到多线程编程时,我想到了加速我的程序的机会,但事实并非如此吗?

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。但为什么呢?是否只是为了以异步方式执行大量查询操作,还是有其他原因?


根据我的回答,尝试使用100个线程而不是5个来运行此程序,并修改为每个套接字扫描5次。在eventlet下,这种工作负载可能更有效。你使用5个线程看不到好处。 - carlsborg
6个回答

8

CPython线程:

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

  • 如果有许多CPython线程同时执行Python代码,则由于全局解释器锁(GIL),一次只能有一个CPython线程解释Python。当它们需要解释Python指令时,其余线程将被阻塞在GIL上。当有许多Python线程时,这会使事情变慢很多。

  • 现在,如果您的Python代码在网络操作(send、connect等)中花费大部分时间: 在这种情况下,争用GIL以解释代码的线程会更少。因此,GIL的效果不是很糟糕。

Eventlet/Green线程:

  • 从上面我们知道,CPython具有线程性能限制。 Eventlets通过在单个核心上运行单个线程并为所有内容使用非阻塞I/O来解决该问题。

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

  • 绿色线程合作地彼此让步,而不是被抢占式地调度。 对于网络操作,套接字库在运行时进行修补(猴子补丁),以使所有调用都是非阻塞的。

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

总结

对于您上面的程序,“真正”的并发(使用CPython版本,在多个处理器上运行5个线程)比事件驱动模型(Eventlet模型,在1个处理器上运行单个线程)要快。

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


这是一个好答案。此外,我们还可以说,CPU绑定任务无法从Python的多线程中获益。如果有人想使用Python处理CPU绑定作业,则最好生成进程而不是线程。绿色线程用于并发,而不是并行。 - Ali Berat Çetin

4
您的问题标题是“Python多线程编程的优势是什么?”,我给您举个例子,而不是试图解决您的问题。我有一个Python程序在我2005年购买的Pentium Core Duo电脑上运行,运行Windows XP系统,从finance.yahoo.com下载500个CSV文件,每个文件大约2K字节,每个文件对应S&P 500中的一只股票。它使用urllib2。如果我不使用线程,需要超过2分钟的时间,使用标准的Python线程(40个线程),则每个文件的平均下载时间为1/4秒,总时间在3到4秒之间(这是墙钟时间,包括计算和I/O)。当我查看每个线程的开始和结束时间(墙钟)时,发现它们之间有很大的重叠。我也用Java程序运行同样的任务,Python和Java的性能几乎相同。使用curllib的C++程序也是如此,但curllib比Java或Python稍微慢一点。我使用的是标准的Python版本2.2.6。

值得指出的是,加速主要来自于不同线程中同时发生的 I/O 操作。 - Rabih Kodeih

3

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

如果您使用类似Cython的东西,则C部分可以并发执行,这就是为什么您会看到速度提升的原因。

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


2
多线程编程的主要优势,无论使用哪种编程语言,都包括:
1. 如果你有一个拥有多个CPU或核心的系统,那么你可以让所有CPU同时执行应用程序代码。例如,如果你有一个拥有四个CPU的系统,在大多数情况下,通过使用多线程,进程可能会以多达4倍的速度运行(尽管在典型的应用程序中,线程需要同步它们对共享资源的访问,这可能会导致创建争用)。
2. 如果进程由于某些原因(磁盘I/O、用户输入、网络I/O)而需要阻塞,在一个或多个线程等待I/O完成时,其他线程可以进行其他工作。请注意,对于这种类型的并发性,你不需要多个CPU或核心,单个CPU上运行的进程也可以从线程中受益。
这些优点是否适用于你的进程,很大程度上取决于你的进程所做的事情。在某些情况下,你将获得显着的性能提升,但在其他情况下,线程版本可能会更慢。请注意,编写良好和高效的多线程应用程序很困难。
现在,既然你特别询问Python,让我们讨论这些优点如何适用于Python。
1. 由于Python中存在全局解释器锁(GIL),因此无法在多个CPU上并行运行代码。GIL确保每次只有一个线程在解释Python代码,因此实际上没有办法充分利用多个CPU。
2. 如果Python线程执行阻塞操作,另一个线程将获取CPU并继续运行,而第一个线程则会被阻塞等待。当阻塞事件完成时,阻塞的线程将恢复。因此,这是在Python脚本中实现多线程的一个好理由(尽管这不是实现此类并发性的唯一方法,非阻塞I/O也可以实现类似的结果)。
以下是从使用多个线程中受益的一些示例:
- 执行耗时操作的GUI程序可以有一个线程继续保持应用程序窗口的刷新和响应,并可能显示长时间操作的进度报告和取消按钮。 - 需要重复从磁盘读取记录,然后对其进行处理,最后将其写回磁盘的进程可以从线程中受益,因为当一个线程被阻塞等待从磁盘获取记录时,另一个线程可以处理已经读取的另一个记录,还有另一个线程可以将另一个记录写回磁盘。如果语言没有GIL(如C ++),那么受益就更大了,因为你可以有多个线程,每个线程都在不同的核心上运行,处理不同的记录。
希望这能帮到你!

1

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

这是有代价的;需要在程序中增加复杂性以调节对共享数据的访问(特别是写入);如果一个线程正在迭代列表,而另一个线程正在更新它,则结果将是不确定的。这也适用于 Python 解释器的内部数据。

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

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


这是一个特定于Python的问题。全局解释器锁意味着这不是真的。 - Eric

0

添加线程并不一定会使进程更快,因为与线程管理相关的开销可能会超过线程带来的任何性能提升。

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


我使用cpython的线程模块重写了应用程序,并获得了真正的速度提升。 - Denis
这是一个特定于Python的问题。 - Eric

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接