Python能够在多个核心上运行吗?

96

问题:由于Python使用了“全局解释器锁(GIL)”,Python能够同时运行其单独的线程吗?


信息:

阅读了这篇文章后,我对Python是否能够利用多核处理器有些不确定。尽管Python非常出色,但它缺乏这样一个强大的能力似乎感觉很奇怪。所以我有点不确定,决定在这里询问。如果我编写一个多线程程序,它能够在多个核心上同时执行吗?


相关:https://dev59.com/NnVC5IYBdhLWcg3wtzoQ - wkl
7
请注意,问题“Python是否能够在多个核心上运行?”与“Python是否能够同时运行其单独的线程(在一个进程中)?”是两个不同的问题。 - Zachary Ryan Smith
7个回答

87
答案是“是的,但是...”
但是当您使用常规线程进行并发时,cPython无法实现。
您可以使用诸如multiprocessing, celerympi4py之类的东西将并行工作拆分到另一个进程中;
或者您可以使用像JythonIronPython这样的东西来使用替代解释器,该解释器没有GIL。
更柔和的解决方案是使用不会违反GIL的库来处理繁重的CPU任务,例如numpy可以完成繁重的工作而不保留GIL,因此其他python线程可以继续。您也可以以这种方式使用ctypes库。
如果您不执行CPU绑定的工作,则可以完全忽略GIL问题(有点),因为在等待IO时python不会获取GIL。

或者Stackless Python,我认为。 - agf
我相信目前pypy仍然有GIL,他们正在尝试软件事务性内存,但还没有完成。 - Jakob Bowyer
看起来你是对的;虽然为了支持没有GIL的PyPy所需的一些工作已经开始(特别是混合垃圾收集器),但GIL仍然存在;我已经编辑了答案以反映这一点。 - SingleNegationElimination
4
考虑到几乎每个人都会回答我的问题,好像我不知道什么是GIL,我感觉你是唯一一个读完整个问题的人...无论如何,谢谢,各种库的链接非常有帮助。 - Narcolapser
你能举几个“等待IO”的实际例子吗?也就是说,什么是常见的IO绑定任务? - Jacob Waters

51

4
+1,我迄今为止的经验是线程在某种程度上受到限制。使用多进程可以解决这个问题! - math
多进程对我不起作用,我已经仔细阅读了文档并尝试了一个带有所有参数的替代方案:jobutil。非常令人沮丧。 - Jamie Nicholl-Shelley

14

CPython(Python的经典和普遍实现)不能同时执行多个线程的Python字节码。这意味着计算密集型程序只会使用一个核心。I/O操作和在C扩展中发生的计算(例如numpy)可以同时进行。

Python的其他实现(如Jython或PyPy)可能有所不同,我对它们的细节了解较少。

通常建议使用许多进程而不是许多线程。


只有一个问题。如果在 CPython 程序中,多个线程仅进行上下文切换而不在不同的 CPU 核心中运行不同的线程,那么我们是否应该选择异步而不是线程? - bad programmer
@badprogrammer,asyncio的IO速度比多线程慢,但它可以处理更多的连接,因此这取决于任务的特定性。 - ruslan_krivoshein

9
如之前的回答所述 - 这取决于 "cpu-bound or i/o bound?" 的答案, 但也取决于 "threaded or multi-processing?" 的答案。例子是在 Raspberry Pi 3B 1.2GHz 的 4 核上运行 Python3.7.3。

对于 IO Bound 测试,多线程和多进程结果相似,但对于 CPU Bound 测试,多进程比多线程更有效率。具体结果如下:

使用线程:

典型结果:
. 启动 4000 个 IO bound 线程循环
. 顺序运行时间: 39.15 秒
. 4 个线程并行运行时间: 18.19 秒
. 2 个线程并行运行时间翻倍: 20.61 秒

典型结果:
. 启动 1000000 个仅 CPU 的线程循环
. 顺序运行时间: 9.39 秒
. 4 个线程并行运行时间: 10.19 秒
. 2 个线程并行运行时间翻倍: 9.58 秒

使用多进程:

典型结果:
. 启动 4000 个 IO bound 进程循环
. 顺序运行时间: 39.74 秒
. 4 个进程并行运行时间: 17.68 秒
. 2 个进程并行运行时间翻倍: 20.68 秒

典型结果:
. 启动 1000000 个仅 CPU 的进程循环
. 顺序运行时间: 9.24 秒
. 4 个进程并行运行时间: 2.59 秒
. 2 个进程并行运行时间翻倍: 4.76 秒

compare_io_multiproc.py:
#!/usr/bin/env python3

# Compare single proc vs multiple procs execution for io bound operation

"""
Typical Result:
  Starting 4000 cycles of io-bound processing
  Sequential - run time: 39.74 seconds
  4 procs Parallel - run time: 17.68 seconds
  2 procs Parallel twice - run time: 20.68 seconds
"""
import time
import multiprocessing as mp

# one thousand
cycles = 1 * 1000

def t():
        with open('/dev/urandom', 'rb') as f:
                for x in range(cycles):
                        f.read(4 * 65535)

if __name__ == '__main__':
    print("  Starting {} cycles of io-bound processing".format(cycles*4))
    start_time = time.time()
    t()
    t()
    t()
    t()
    print("  Sequential - run time: %.2f seconds" % (time.time() - start_time))

    # four procs
    start_time = time.time()
    p1 = mp.Process(target=t)
    p2 = mp.Process(target=t)
    p3 = mp.Process(target=t)
    p4 = mp.Process(target=t)
    p1.start()
    p2.start()
    p3.start()
    p4.start()
    p1.join()
    p2.join()
    p3.join()
    p4.join()
    print("  4 procs Parallel - run time: %.2f seconds" % (time.time() - start_time))

    # two procs
    start_time = time.time()
    p1 = mp.Process(target=t)
    p2 = mp.Process(target=t)
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    p3 = mp.Process(target=t)
    p4 = mp.Process(target=t)
    p3.start()
    p4.start()
    p3.join()
    p4.join()
    print("  2 procs Parallel twice - run time: %.2f seconds" % (time.time() - start_time))

compare_cpu_multiproc.py
#!/usr/bin/env python3

# Compare single proc vs multiple procs execution for cpu bound operation

"""
Typical Result:
  Starting 1000000 cycles of cpu-only processing
  Sequential run time: 9.24 seconds
  4 procs Parallel - run time: 2.59 seconds
  2 procs Parallel twice - run time: 4.76 seconds
"""
import time
import multiprocessing as mp

# one million
cycles = 1000 * 1000

def t():
    for x in range(cycles):
        fdivision = cycles / 2.0
        fcomparison = (x > fdivision)
        faddition = fdivision + 1.0
        fsubtract = fdivision - 2.0
        fmultiply = fdivision * 2.0

if __name__ == '__main__':
    print("  Starting {} cycles of cpu-only processing".format(cycles))
    start_time = time.time()
    t()
    t()
    t()
    t()
    print("  Sequential run time: %.2f seconds" % (time.time() - start_time))

    # four procs
    start_time = time.time()
    p1 = mp.Process(target=t)
    p2 = mp.Process(target=t)
    p3 = mp.Process(target=t)
    p4 = mp.Process(target=t)
    p1.start()
    p2.start()
    p3.start()
    p4.start()
    p1.join()
    p2.join()
    p3.join()
    p4.join()
    print("  4 procs Parallel - run time: %.2f seconds" % (time.time() - start_time))

    # two procs
    start_time = time.time()
    p1 = mp.Process(target=t)
    p2 = mp.Process(target=t)
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    p3 = mp.Process(target=t)
    p4 = mp.Process(target=t)
    p3.start()
    p4.start()
    p3.join()
    p4.join()
    print("  2 procs Parallel twice - run time: %.2f seconds" % (time.time() - start_time))



5

以下是可以占用ubuntu 14.04上所有4个核心的示例代码,该系统使用的是Python 2.7 64位版本。

import time
import threading


def t():
    with open('/dev/urandom') as f:
        for x in xrange(100):
            f.read(4 * 65535)

if __name__ == '__main__':
    start_time = time.time()
    t()
    t()
    t()
    t()
    print "Sequential run time: %.2f seconds" % (time.time() - start_time)

    start_time = time.time()
    t1 = threading.Thread(target=t)
    t2 = threading.Thread(target=t)
    t3 = threading.Thread(target=t)
    t4 = threading.Thread(target=t)
    t1.start()
    t2.start()
    t3.start()
    t4.start()
    t1.join()
    t2.join()
    t3.join()
    t4.join()
    print "Parallel run time: %.2f seconds" % (time.time() - start_time)

结果:

$ python 1.py
Sequential run time: 3.69 seconds
Parallel run time: 4.82 seconds

5
并行运行时间比顺序运行时间更差。:o - sliders_alpha
2
在这里,你会失去上下文切换的优势。真正对你有益的是,如果你正在与 Web 服务交互,其中大部分时间都花在等待响应上。 - chjortlund
我认为这只是一个异步线程,而不是来自C pthread库的pthread类型线程。 - Fahim Ferdous
你需要使用多进程库来完成你想要做的事情。 - Fahim Ferdous
这个回答的意义是什么?你是想展示你的代码有多糟糕吗? - Nike

3
我把脚本转换成了Python3,并在我的Raspberry Pi 3B+上运行它:
import time
import threading

def t():
        with open('/dev/urandom', 'rb') as f:
                for x in range(100):
                        f.read(4 * 65535)

if __name__ == '__main__':
    start_time = time.time()
    t()
    t()
    t()
    t()
    print("Sequential run time: %.2f seconds" % (time.time() - start_time))

    start_time = time.time()
    t1 = threading.Thread(target=t)
    t2 = threading.Thread(target=t)
    t3 = threading.Thread(target=t)
    t4 = threading.Thread(target=t)
    t1.start()
    t2.start()
    t3.start()
    t4.start()
    t1.join()
    t2.join()
    t3.join()
    t4.join()
    print("Parallel run time: %.2f seconds" % (time.time() - start_time))

python3 t.py

Sequential run time: 2.10 seconds
Parallel run time: 1.41 seconds

对我而言,使用并行运行更加快速。


所以它是并行运行的,但它会在不同的核心上运行吗? - Maciek Woźniak

2

线程共享一个进程,进程在核心上运行。但是你可以使用Python的multiprocessing模块在不同的进程中调用函数并利用其他核心,或者你可以使用subprocess模块运行你的代码和非Python代码。


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