我的Python进程运行在哪些CPU核心上?

45

设置

我用Python在Windows PC上编写了一段相当复杂的软件。我的软件基本上启动了两个Python解释器窗口。第一个窗口在双击main.py文件时启动(我猜测)。在这个窗口中,会以以下方式启动其他线程:

    # Start TCP_thread
    TCP_thread = threading.Thread(name = 'TCP_loop', target = TCP_loop, args = (TCPsock,))
    TCP_thread.start()

    # Start UDP_thread
    UDP_thread = threading.Thread(name = 'UDP_loop', target = UDP_loop, args = (UDPsock,))
    TCP_thread.start()

Main_thread 启动了一个 TCP_thread 和一个 UDP_thread。虽然它们是独立的线程,但它们都在同一个 Python shell 中运行。

Main_thread 还启动了一个子进程。操作步骤如下:

p = subprocess.Popen(['python', mySubprocessPath], shell=True)

根据Python文档,我理解这个子进程在一个单独的Python解释器会话/Shell中同时运行。此子进程中的Main_thread完全专用于我的GUI。GUI为其所有通信启动了一个TCP_thread

我知道事情变得有点复杂。因此,我已经在这张图片中总结了整个设置:

enter image description here


我有几个关于这个设置的问题。我将在这里列出它们:

问题1 [已解决]

Python解释器一次只使用一个CPU核心来运行所有线程,这是真的吗?换句话说,Python解释器会话1(来自图中)会在一个CPU核心上运行所有3个线程(Main_threadTCP_threadUDP_thread)吗?

答案:是的,这是正确的。GIL(全局解释器锁)确保所有线程一次只能在一个CPU核心上运行。

问题2 [尚未解决]

我有没有办法跟踪它是哪个CPU核心?

问题3 [部分解决]

对于这个问题,我们忘记线程,而是专注于Python中的子进程机制。启动一个新的子进程意味着启动一个新的Python解释器实例。这是正确的吗?

答案:是的,这是正确的。起初有些混淆,不确定以下代码是否会创建一个新的Python解释器实例:

    p = subprocess.Popen(['python', mySubprocessPath], shell = True)

问题已经明确。这段代码确实会启动一个新的Python解释器实例。

Python是否足够智能,能让该独立的Python解释器实例在不同的CPU核心上运行?也许有一些零星的打印语句可以追踪它运行在哪个核心上吗?

问题 4 [新问题]

社区讨论提出了一个新问题。在生成一个新进程(在新的Python解释器实例中)时,显然有两种方法:

    # Approach 1(a)
    p = subprocess.Popen(['python', mySubprocessPath], shell = True)

    # Approach 1(b) (J.F. Sebastian)
    p = subprocess.Popen([sys.executable, mySubprocessPath])

    # Approach 2
    p = multiprocessing.Process(target=foo, args=(q,))
第二种方法的明显缺点是它只针对一个函数 - 而我需要打开一个新的Python脚本。不管怎样,这两种方法在实现上是否相似?

第二种方法的明显缺点是它只针对一个函数 - 而我需要打开一个新的Python脚本。不管怎样,这两种方法在实现上是否相似?


https://docs.python.org/2/library/multiprocessing.html - mootmoot
3
我认为你应该质疑自己为什么关心线程在哪个物理核心上运行。操作系统通常会根据各种因素在可用的CPU之间移动线程。你想监视和/或干涉这个过程有什么特别的原因吗? - Dolda2000
我认为你的说法“操作系统将始终让它们同时运行”在Python中不成立。Python有一个GIL(全局解释器锁),以确保您的软件不会执行多核操作。如果您想在多个CPU核心上执行它,需要绕过此GIL。我只是Python的初学者,所以请随时纠正我错误的地方。您是正确的,Python不是处理CPU密集型任务的最佳选择。我选择Python的原因更多是与我的同事共享知道Python的软件有关。无论如何,这超出了问题的范围 :-) - K.Mulier
只要我写的是“可运行”的线程,它就是成立的。由于GIL的存在,每个Python进程(试图运行Python字节码)一次只能运行一个可运行线程,其他线程会在GIL上阻塞。 - Dolda2000
1
此外,我并不怀疑您选择的编程语言。只是说,只要您在运行Python代码,那么担心线程在哪些具体物理核心上运行超出了您的性能问题的范围。像CPU绑定之类的问题只有在需要避免虚假高速缓存刷新等非常细粒度的情况下才会变得相关,而这些都远远超出了在运行Python代码时会有所区别的范畴。 - Dolda2000
显示剩余8条评论
3个回答

33

问:Python解释器一次只能使用一个CPU核心来运行所有线程,这是真的吗?

不是。GIL和CPU亲和力是无关的概念。在阻塞I/O操作、C扩展内的长时间CPU密集型计算中,GIL可以被释放。

如果一个线程被阻塞在GIL上,它可能没有任何CPU核心,因此可以说纯Python多线程代码在CPython实现上一次只能使用一个CPU核心。

问:换句话说,Python解释器会在一个CPU核心上运行所有3个线程(Main_thread、TCP_thread和UDP_thread),对吗?

我认为CPython没有隐式地管理CPU亲和性。它很可能依赖于操作系统调度程序来选择在哪个线程上运行。Python线程是在真正的操作系统线程上实现的。

问:还是说Python解释器能够将它们分布在多个核心上?

要找出可用CPU的数量:

>>> import os
>>> len(os.sched_getaffinity(0))
16

无论线程是否在不同的CPU上调度,都不取决于Python解释器。

问:假设问题1的答案是“多个核心”,我是否有办法跟踪每个线程运行在哪个核心上,或许可以通过一些零散的打印语句?如果问题1的答案是“只有一个核心”,我是否有办法追踪它是哪一个核心?

我想象中,特定的CPU可能会在不同的时间段发生变化。您可以查看旧版Linux内核上类似于/proc/<pid>/task/<tid>/status的内容。在我的机器上,task_cpu可以从/proc/<pid>/stat/proc/<pid>/task/<tid>/stat读取

>>> open("/proc/{pid}/stat".format(pid=os.getpid()), 'rb').read().split()[-14]
'4'

对于当前的便携式解决方案,请查看psutil是否公开了此类信息。

您可以将当前进程限制在一组CPU上:

os.sched_setaffinity(0, {0}) # current process on 0-th core

问题:在这个问题中,我们忘记线程,但专注于Python中的子进程机制。启动一个新的子进程意味着启动一个新的Python解释器会话/ shell。这是正确的吗?

是的。 subprocess模块创建新的操作系统进程。如果您运行python可执行文件,则会启动新的Python解释器。如果您运行bash脚本,则不会创建新的Python解释器,即运行bash可执行文件不会启动新的Python解释器/会话等。

问题:假设这是正确的,那么Python是否足够聪明,可以使该单独的解释器会话在不同的CPU核心上运行?有没有一种方法来跟踪这一点,也许使用一些零散的打印语句?

请参见上文(即,操作系统决定在哪里运行您的线程,并且可能存在公开线程所在位置的操作系统API)。

multiprocessing.Process(target=foo, args=(q,)).start()

multiprocessing.Process还会创建一个新的操作系统进程(运行新的Python解释器)。

实际上,我的子进程是另一个文件。所以这个例子对我不起作用。

Python使用模块来组织代码。如果你的代码在另一个文件another_file.py中,那么在主模块中import another_file并将another_file.foo传递给multiprocessing.Process
尽管如此,您如何将其与p = subprocess.Popen(..)进行比较?如果我使用subprocess.Popen(..)启动新进程(或者应该说是'python解释器实例'),这是否重要?multiprocessing.Process()很可能是基于subprocess.Popen()实现的。 multiprocessing提供了类似于threading API的API,并且它抽象出了Python进程之间通信的细节(如何序列化Python对象以便在进程之间发送)。
如果没有CPU密集型任务,那么您可以在单个进程中运行GUI和I/O线程。如果有一系列的CPU密集型任务,则为了同时利用多个CPU,可以使用多个线程与C扩展(如lxmlregexnumpy(或使用Cython创建的自己的扩展)),这些扩展可以在长时间计算期间释放GIL,或将它们转移到单独的进程中(一个简单的方法是使用concurrent.futures提供的进程池)。

Q: The community discussion raised a new question. There are apparently two approaches when spawning a new process (within a new Python interpreter instance):

# Approach 1(a)
p = subprocess.Popen(['python', mySubprocessPath], shell = True)

# Approach 1(b) (J.F. Sebastian)
p = subprocess.Popen([sys.executable, mySubprocessPath])

# Approach 2
p = multiprocessing.Process(target=foo, args=(q,))
"方法1(a)"在POSIX上是错误的(尽管在Windows上可能有效)。为了可移植性,请使用“方法1(b)”,除非您知道需要cmd.exe(在这种情况下传递一个字符串,以确保使用正确的命令行转义)。

第二种方法的明显缺点是它只针对一个函数 - 而我需要打开一个新的Python脚本。无论如何,这两种方法在实现目标方面是否相似?

subprocess创建新进程,任何进程,例如,您可以运行一个bash脚本。 multprocessing用于在另一个进程中运行Python代码。导入Python模块并运行其函数比将其作为脚本运行更灵活。请参见使用subprocess在Python脚本内调用带输入的python脚本

"

在您的第一个问题/答案中,您是否暗示在阻塞IO操作存在的情况下,纯Python多线程(但不是多进程)应用程序可能涉及到多个核心? - Serge
@Serge 我不明白它与你的问题有什么关系。你是在问不同的CPython进程是否有自己的GIL吗?(答案是肯定的)。显然,不同的Python进程可以同时在不同的CPU上运行(甚至在不同的主机上)。 - jfs
不对,根据文档,锁在可能会阻塞 I/O 操作(例如读写文件)时也会被释放,以便其他 Python 线程可以同时运行。这是指它在进行I/O操作之前会释放锁并重新获取,还是读取文件内容可能与另一个线程并行进行,使用另一个内核。 - Serge
我很好奇一个纯Python应用程序在多线程下可以使用多少个核心,但不是多进程。 - Serge
文档实际上在同一页中说只有带有GIL的线程才能执行,但一些程序员声称他们看到多线程应用程序使用了多个核心。 - Serge
显示剩余3条评论

3

由于您正在使用构建在thread上的threading模块。正如文档所示,它使用您操作系统的''POSIX线程实现''pthread

  1. 线程由操作系统而不是Python解释器管理。因此答案将取决于您系统中的pthread库。但是,CPython使用GIL防止多个线程同时执行Python字节码。因此它们将被序列化。但仍然可以将它们分离到不同的核心中,这取决于您的pthread libs。
  2. 简单地使用调试器并将其附加到您的python.exe上。例如GDB thread command
  3. 类似于问题1,新进程由您的操作系统管理,可能在不同的核心上运行。使用调试器或任何进程监视器来查看它。有关更多详细信息,请访问CreatProcess()文档page

1

1、2:您有三个真实的线程,但在CPython中,它们受GIL限制,因此假设它们运行纯Python代码,您将看到CPU使用率就像只使用一个核心。

3:如gdlmx所说,选择要在哪个核心上运行线程取决于操作系统, 但如果您真的需要控制,可以使用本机API通过ctypes设置进程或线程亲和力。由于您在Windows上,应该是这样的:

# This will run your subprocess on core#0 only
p = subprocess.Popen(['python', mySubprocessPath], shell = True)
cpu_mask = 1
ctypes.windll.kernel32.SetProcessAffinityMask(p._handle, cpu_mask)

我在这里使用私有的Popen._handle,以简化操作。正确的方法是使用OpenProcess(p.tid)等方式。
是的,subprocess像其他一切一样在另一个新进程中运行Python。

(3)完美地工作。如果没有它,一个简单的单线程忙循环将被扔到所有内核周围;有了那段代码,它永久在核心#0上。我不知道它是否有用,但也许有一些与缓存相关的原因可以使关键线程保持在一个单独的核心上。 - max

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