何时使用线程,以及使用多少个线程

7
我有一个工作项目。我们编写了一个模块,并有一个#TODO来实现线程以改进该模块。我是一个相对新的Python程序员,决定尝试一下。在学习和实现线程时,我有一个类似于太多线程会有多少个?的问题,因为我们有一个大约有6个对象需要处理的队列,所以为什么要创建6个线程(或者根本不创建任何线程)来处理列表或队列中的对象,当处理时间无关紧要时呢? (每个对象最多需要大约2秒钟的处理时间)
所以我进行了一个小实验。我想知道使用线程是否会有性能提升。请参见我的Python代码:
import threading
import queue
import math
import time

results_total = []
results_calculation = []
results_threads = []

class MyThread(threading.Thread):
    def __init__(self, thread_id, q):
        threading.Thread.__init__(self)
        self.threadID = thread_id
        self.q = q

    def run(self):
        # print("Starting " + self.name)
        process_data(self.q)
        # print("Exiting " + self.name)


def process_data(q):
    while not exitFlag:
        queueLock.acquire()
        if not workQueue.empty():
            potentially_prime = True
            data = q.get()
            queueLock.release()
            # check if the data is a prime number
            # print("Testing {0} for primality.".format(data))
            for i in range(2, int(math.sqrt(data)+1)):
                if data % i == 0:
                    potentially_prime = False
                    break
            if potentially_prime is True:
                prime_numbers.append(data)
        else:
            queueLock.release()

for j in [1, 2, 3, 4, 5, 10, 15, 20, 25, 30, 40, 50, 75, 100, 150, 250, 500,
          750, 1000, 2500, 5000, 10000]:
    threads = []
    numberList = list(range(1, 10001))
    queueLock = threading.Lock()
    workQueue = queue.Queue()
    numberThreads = j
    prime_numbers = list()
    exitFlag = 0

    start_time_total = time.time()
    # Create new threads
    for threadID in range(0, numberThreads):
        thread = MyThread(threadID, workQueue)
        thread.start()
        threads.append(thread)

    # Fill the queue
    queueLock.acquire()
    # print("Filling the queue...")
    for number in numberList:
        workQueue.put(number)
    queueLock.release()
    # print("Queue filled...")
    start_time_calculation = time.time()
    # Wait for queue to empty
    while not workQueue.empty():
        pass

    # Notify threads it's time to exit
    exitFlag = 1

    # Wait for all threads to complete
    for t in threads:
        t.join()
    # print("Exiting Main Thread")
    # print(prime_numbers)
    end_time = time.time()
    results_total.append(
            "The test took {0} seconds for {1} threads.".format(
                end_time - start_time_total, j)
            )
    results_calculation.append(
            "The calculation took {0} seconds for {1} threads.".format(
                    end_time - start_time_calculation, j)
            )
    results_threads.append(
            "The thread setup time took {0} seconds for {1} threads.".format(
                    start_time_calculation - start_time_total, j)
            )
for result in results_total:
    print(result)
for result in results_calculation:
    print(result)
for result in results_threads:
    print(result)

这个测试找出了1到10000之间的质数。这个设置基本上是从https://www.tutorialspoint.com/python3/python_multithreading.htm中直接采用的,但我要求线程查找质数而不是打印简单的字符串。这实际上并不是我的真实应用程序,但我目前无法测试我为该模块编写的代码。我认为这是一个衡量额外线程效果的好测试。我的真实世界应用程序涉及与多个串行设备通信。我运行了5次测试并平均了时间。以下是图表结果:

Test Time vs. Number of Threads

关于线程和这个测试,我的问题如下:

  1. 这个测试是否是线程使用的好代表?这不是服务器/客户端情况。在效率方面,当你没有为客户服务或处理添加到队列中的任务/工作时,避免并行处理是否更好?

  2. 如果问题1的答案是“不,这个测试不是使用线程的场所。”那么什么时候可以使用线程?一般来说。

  3. 如果问题1的答案是“是的,在这种情况下使用线程是可以的。”为什么添加线程最终需要更长的时间并迅速达到平台期?而且,为什么要使用线程,因为它比在循环中计算要花费多倍的时间。

我注意到当工作与线程的比例越接近1:1时,设置线程所需的时间越长。因此,线程仅在您创建线程一次并尽可能长时间保持它们活动以处理可能比它们可以计算的请求更快的请求时才有用吗?


这个问题应该有一个与主题相关的名称。这样做的想法是为了帮助未来有同样问题的人们得到答案。 - dogoncouch
3个回答

12

不,这不是使用线程的好地方。

一般情况下,您希望在代码受IO限制时使用线程;也就是说,它花费了大量时间等待输入或输出。例如,以并行方式从URL列表下载数据;代码可以在等待前一个URL返回的同时开始请求下一个URL的数据。

但这里的情况不是这样的;计算质数是受CPU限制的。


1
在 CPU 密集型的情况下,multiprocessing 是一个值得关注的好库。它可以绕过 GIL 并能够使用 CPU 的不同核心。https://docs.python.org/2/library/multiprocessing.html - dedsec

4

你认为在这里使用多线程是有问题的,这是正确的想法。多线程本身很棒,在合适的应用场景下可以大大缩短运行时间。

然而,从另一方面来看,它还会给实现它的任何程序增加额外的复杂度(尤其是在Python中)。使用多线程也需要考虑时间代价,例如进行上下文切换时发生的时间或实际创建线程所需的时间等。

当你的程序需要处理数千个资源密集型任务时,这些时间代价就可以忽略不计,因为使用多线程可以节省大量时间。但就你的情况而言,我不确定你的需求是否满足这些要求。我没有深入研究你要处理的对象类型,但你说它们只需要大约2秒钟,这并不可怕,而且你还说每次只有6个项需要处理。因此,我们可以预计你的脚本的主要部分平均运行12秒钟。在我看来,这并不需要使用多线程,因为在一条线程中,你的Python脚本已经在那段时间内很好地处理第二个对象了。

简而言之,除非你需要,否则不要使用多线程。例如,像基因测序这样的大型数据集(Python中的大事情)因为多个线程可以帮助并发处理这些庞大的文件,所以受益匪浅。在你的情况下,看起来收效甚微。希望这会有所帮助。


3
在Python中,线程用于同时运行多个线程(任务、函数调用)。请注意,这并不意味着它们在不同的CPU上执行。如果您的程序已经使用了100%的CPU时间,Python线程将无法使其更快。在这种情况下,您可能需要考虑并行编程。
由于GIL机制,这就是为什么线程在Python中只有在您拥有IO绑定代码时才有用的原因。但是,对于IO绑定代码,最好使用轻量级线程在某些事件循环的顶部运行(使用gevent、eventlet、asyncio或类似工具),因为这样您可以轻松地运行数百个(甚至更多)并行操作,而每个线程的开销非常小。
如果您想要使用多个CPU核心来加速执行,请查看multiprocessing模块。

在大多数情况下,Python 中的线程确实可以利用多个 CPU 核心。但它不能利用多个处理器。异步编程并不能真正意义上并行运行任务,只是利用线程在等待服务器响应时发生的空闲时间。为了获得最佳性能,最好将异步编程与多线程甚至多进程相结合使用。 - Ionut Hulub
2
@IonutHulub 你错了。在大多数情况下,线程不会使用超过1个CPU核心,特别是当处理任何类型的计算时,这个问题就是关于计算的。这里你可以了解更多信息。你似乎也误解了我关于异步的写法 - 因为我从未说过它允许并行处理。 - vith

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