为什么python线程如此令人沮丧?

时间:2013-09-03 06:14:35

标签: python multithreading

# test.py

import threading
import time
import random
from itertools import count

def fib(n):
  """fibonacci sequence
  """
  if n < 2:
    return n
  else:
    return fib(n - 1) + fib(n - 2)

if __name__ == '__main__':
    counter = count(1)
    start_time = time.time()
    def thread_worker():
        while True:
            try:
                # To simulate downloading
                time.sleep(random.randint(5, 10))
                # To simulate doing some process, will take about 0.14 ~ 0.63 second
                fib(n=random.randint(28, 31))
            finally:
                finished_number = counter.next()
                print 'Has finished %d, the average speed is %f per second.' % (finished_number, finished_number/(time.time() - start_time))

    threads = [threading.Thread(target=thread_worker) for i in range(100)]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()

以上是我的测试脚本。 thread_worker函数最多运行10.63秒才能运行一次。 我开始了100个线程,预计结果是每秒~10次。 但实际结果令人沮丧如下:

...
Has finished 839, the average speed is 1.385970 per second.
Has finished 840, the average speed is 1.386356 per second.
Has finished 841, the average speed is 1.387525 per second.
...

如果我评论“fib(n = random.randint(28,31))”,结果是预期的:

...
Has finished 1026, the average speed is 12.982740 per second.
Has finished 1027, the average speed is 12.995230 per second.
Has finished 1028, the average speed is 13.007719 per second.
...

已完成1029,平均速度为每秒12.860571。

我的问题是为什么它这么慢?我预计每秒约10。 如何让它更快? fib()函数只是模拟做一些过程。例如从大html中提取数据。

4 个答案:

答案 0 :(得分:7)

如果我要你烤蛋糕,这需要你一个半小时,面团需要30分钟,烤箱需要60分钟,根据你的逻辑,我希望2个蛋糕可以花费相同的时间。但是有些事情你会遗漏。首先,如果我不告诉你在开始时烤两个蛋糕,你必须做两次面团,现在是2次30分钟。现在它实际上需要你两个小时(你可以自由地工作第二个蛋糕,第一个是在烤箱里)。

现在让我们假设我要求你烤四个蛋糕,我不允许你做面团一次并分成四块蛋糕,但你必须每次都做。我们现在所期待的时间是4 * 30分钟+一小时的烘烤蛋糕。现在为了举例,假设你的妻子帮助你,这意味着你可以平行地做两个蛋糕的面团。现在预计的时间是两个小时,因为每个人都要烤两个蛋糕。但是你的烤箱一次只能装2个蛋糕。现在时间变成30分钟来制作第一块面团,1小时烘烤它,而你制作第二块面团,在前两个蛋糕完成后,你将下两个蛋糕放入烤箱。需要一个小时。如果你把时间加起来,你会发现它现在花了你两个半小时。

如果你进一步采取这种做法,我会问你一千个蛋糕需要500个半小时。

这与线程有什么关系?

考虑将面团作为初始计算,创建100%的CPU负载。你的妻子是双核的第二个核心。烤箱是一种资源,您的程序会为其生成50%的负载。

在真正的线程中,你有一些开销线程的开销(我告诉你要烘烤蛋糕,你必须要求你的妻子帮忙需要时间),你争夺资源(即记忆访问)(你和你的妻子)不能同时使用混音器。即使线程数小于核心数,加速也是亚线性的。

此外,智能程序在主线程中下载一次代码(使面团一次),然后将其复制到线程,没有必要复制计算。它只是因为你计算了两次而没有让它更快。

答案 1 :(得分:4)

虽然Manoj's answer是正确的,但我认为需要更多解释。 python GIL是cpython中使用的互斥锁,它实际上将禁用python代码的任何并行执行。它不会使线程代码变慢,也不会实际阻止操作系统在所有内核上同时调度python线程。它只是确保只有一个线程可以同时执行python字节代码。

这对你意味着什么?你基本上做了两件事:

  1. 睡眠:执行此功能时,没有执行任何python代码,您只需要5到10秒钟就可以执行任何操作。与此同时,任何其他线程都可以做同样的事情。鉴于调用time.sleep的开销可以忽略不计,您可能拥有数千个线程,并且它可能仍然会像您预期的那样线性扩展。这就是为什么一旦您注释掉fib行,一切正常。您的平均睡眠时间为7.5s,因此您希望每秒进行15次计算。
  2. Fibonacci序列的计算:这是问题所在,它实际上是在执行python代码。假设每次计算大约需要0.5秒。现在我们已经看到,无论你有多少线程,你当时只能运行一个计算。鉴于此,您每秒只能进行2次计算。
  3. 现在,它低于152,主要是因为涉及一些开销。首先,您要将数据打印到屏幕上,这几乎总是一个非常缓慢的操作。其次,你使用100个线程,这意味着你不断在100个线程堆栈之间切换(即使它们正在休眠),这不是一个轻量级的操作。

    请注意,线程仍然非常有用。例如,阻塞调用,其中执行不是由python本身执行,而是由其他资源执行。这可能是等待套接字的结果,像你的例子中的睡眠,甚至是在python本身之外进行的计算(例如,许多numpy计算)。

答案 2 :(得分:0)

Python线程使用Global Interpretor Lock(GIL)来同步Python解释器状态的访问。与其他线程(如POSIX线程)相比,使用GIL可以使Python线程明显变慢,尤其是在处理多个内核时。这是众所周知的。这是一个非常好的演示文稿:www.dabeaz.com/python/UnderstandingGIL.pdf‎

答案 3 :(得分:0)

您正在寻找更快的解决方案。记忆结果有帮助。

import collections
import functools


class Memoized(object):
    """Decorator. Caches a function's return value each time it is called.
    If called later with the same arguments, the cached value is returned
    (not reevaluated).
    """

    def __init__(self, func):
        self.func = func
        self.cache = {}

    def __call__(self, *args):
        if not isinstance(args, collections.Hashable):
            # uncacheable. a list, for instance.
            # better to not cache than blow up.
            return self.func(*args)
        if args in self.cache:
            return self.cache[args]
        else:
            value = self.func(*args)
            self.cache[args] = value
            return value

    def __repr__(self):
        """Return the function's docstring."""
        return self.func.__doc__

    def __get__(self, obj, objtype):
        """Support instance methods."""
        return functools.partial(self.__call__, obj)


if __name__ == '__main__':
    @Memoized
    def fibonacci(n):
        """Return the nth fibonacci number

        :param n: value
        """
        if n in (0, 1):
            return n
        return fibonacci(n - 1) + fibonacci(n - 2)

    print(fibonacci(35))

尝试在有和没有@Memoized装饰器的情况下运行它。

http://wiki.python.org/moin/PythonDecoratorLibrary#Memoize获取食谱。