什么是 Python 线程?

46

关于 Python 线程,我有几个问题。

  1. Python 线程是 Python 实现还是操作系统实现?
  2. 当我使用 htop 查看多线程脚本时,会出现多个条目——相同的内存消耗、相同的命令但不同的 PID。这是否意味着 [Python] 线程实际上是一种特殊类型的进程?(我知道 htop 中有一个设置,可以将这些线程显示为一个进程 —— 隐藏用户态线程
  3. 文档 中说:

线程可以被标记为“守护线程”。此标志的意义在于,当只剩下守护线程时,整个 Python 程序就退出了。

我的解释/理解是:当所有非守护线程都终止时,主线程才终止。

因此,如果“当只剩下守护线程时,整个 Python 程序就退出了”,那么 Python 守护线程不是 Python 程序的一部分吗?


你是指 threading.Thread 对吗? - David Heffernan
是的,有。标准Python中还有其他线程吗? - warvariuc
是的,thread 模块提供了另一种接口来使用本地线程(但它们使用相同的本地实现)。 - Sylvain Defresne
4个回答

41
  1. 在我知道的所有实现中(C Python、PyPy 和 Jython),Python 线程都是使用 OS 线程实现的。对于每个 Python 线程,都有一个底层的 OS 线程。

  2. 一些操作系统(例如 Linux)会在所有正在运行的进程列表中显示同一可执行文件启动的所有不同线程。这是操作系统的实现细节,而不是 Python 的。在其他一些操作系统上,列出所有进程时可能看不到这些线程。

  3. 当最后一个非守护线程完成时,进程将终止。此时,所有守护线程都将被终止。因此,这些线程是您的进程的一部分,但不会阻止其终止(而常规线程会阻止它)。这是纯 Python 实现的。当系统调用 _exit 函数时,进程终止(它将杀死所有线程),当主线程终止(或调用 sys.exit)时,Python 解释器会检查是否有另一个非守护线程正在运行。如果没有,则调用 _exit,否则等待非守护线程完成。


守护线程标志是由 threading 模块在纯 Python 中实现的。当加载模块时,会创建一个 Thread 对象来表示主线程,并将其 _exitfunc 方法注册为 atexit 钩子。

该函数的代码如下:

class _MainThread(Thread):

    def _exitfunc(self):
        self._Thread__stop()
        t = _pickSomeNonDaemonThread()
        if t:
            if __debug__:
                self._note("%s: waiting for other threads", self)
        while t:
            t.join()
            t = _pickSomeNonDaemonThread()
        if __debug__:
            self._note("%s: exiting", self)
        self._Thread__delete()

当调用sys.exit或主线程终止时,Python解释器将调用此函数。当函数返回时,解释器将调用系统的_exit函数。当只有守护线程在运行时(如果有),函数将终止。
当调用_exit函数时,操作系统将终止所有进程线程,然后终止该进程。直到所有非守护线程完成,Python运行时才会调用_exit函数。
所有线程都是进程的一部分。
引用块中提到:“我的理解是:当所有非守护线程终止时,主线程终止。”因此,“当只剩下守护线程时整个Python程序退出”意味着Python守护线程不是Python程序的一部分吗?
你的理解是错误的。对于操作系统而言,一个进程由许多线程组成,所有线程都是平等的(除了C运行时在main函数末尾添加了对_exit的调用之外,操作系统对主线程没有任何特殊处理)。操作系统不知道守护线程。这纯粹是Python概念。
Python解释器使用本地线程实现Python线程,但必须记住创建的线程列表。并使用其atexit钩子确保_exit函数仅在最后一个非守护线程终止时返回给操作系统。在使用“整个Python程序”时,文档指的是整个进程。
以下程序可以帮助理解守护线程和常规线程之间的区别:
import sys
import time
import threading

class WorkerThread(threading.Thread):

    def run(self):
        while True:
            print 'Working hard'
            time.sleep(0.5)

def main(args):
    use_daemon = False
    for arg in args:
        if arg == '--use_daemon':
            use_daemon = True
    worker = WorkerThread()
    worker.setDaemon(use_daemon)
    worker.start()
    time.sleep(1)
    sys.exit(0)

if __name__ == '__main__':
    main(sys.argv[1:])

如果您使用“--use_daemon”参数运行此程序,您会发现程序只会打印少量的“Working hard”行。如果没有这个参数,即使主线程完成,程序也不会终止,并且会一直打印“Working hard”行,直到被杀死。


当最后一个非守护线程结束时,该进程将终止。那么,守护线程不是Python应用程序进程的一部分吗?我以为守护线程和非守护线程之间的唯一区别就是一个标志,它确定线程的处理方式,正如文档中所述:“这个标志的重要意义在于当只剩下守护线程时,整个Python程序退出。”这里的“整个Python程序”是什么意思?我认为它是指进程。但是,如果进程仍然有线程,它如何被终止? - warvariuc
更新了我的答案,解释了守护线程在Python中的实现方式。 - Sylvain Defresne
从你的回答和例子来看,我会这样说: 一个线程可以被标记为“守护线程”。这个标志的意义在于,当只剩下守护线程时,整个Python程序就会退出(即强制杀死守护线程)。这个理解正确吗? - warvariuc
@warvariuc 是的,就是这样。守护进程标志只是一个标志,表示线程不是关键的,可以被无情地杀死。 - Sylvain Defresne
我试图捕获终止 - 在__del__中添加一些print - 但没有任何输出。它被操作系统杀死了吗? - warvariuc
显示剩余4条评论

16

我不熟悉具体实现,所以让我们进行一个实验:

import threading
import time

def target():
    while True:
        print 'Thread working...'
        time.sleep(5)

NUM_THREADS = 5

for i in range(NUM_THREADS):
    thread = threading.Thread(target=target)
    thread.start()
  1. 使用ps -o cmd,nlwp <pid>命令报告的线程数为NUM_THREADS+1(一个主线程加上一个),因此只要操作系统工具检测到线程数量,它们就应该是操作系统线程。我在cpython和jython中都尝试过,尽管在jython中有一些其他线程在运行,但对于每个额外的线程,ps都会将线程计数增加一。

  2. 我不确定htop的行为如何,但ps似乎是一致的。

  3. 在开启线程之前,我添加了以下行:

  4. thread.daemon = True
    
    当我使用cpython执行程序时,程序几乎立即终止,并且在使用ps命令时找不到任何进程,因此我的猜测是程序已经与线程一起终止。在jython中,程序的工作方式相同(它没有终止),因此可能有一些来自jvm的其他线程阻止程序终止,或者守护线程不受支持。
    注:我使用的是带有python 2.7.2+和jython 2.2.1 on java1.6.0_23的Ubuntu 11.10。

5
  1. Python的线程实际上是解释器的一种实现,因为所谓的全局解释器锁 (GIL),即使在技术上使用操作系统级别的线程机制。在*nix系统中,它利用了pthread,但GIL使其有效地成为一个混合体,粘在应用程序级别的线程范例中。因此,在ps/top输出中,您将在*nix系统上看到它多次出现,但性能方面仍然像是软件实现的线程。

  2. 不,你只是看到了你的操作系统下的底层线程实现方式。这种行为由*nix pthread样式的线程或者我听说甚至Windows也是以这种方式实现线程。

  3. 当您的程序关闭时,它会等待所有线程完成。如果您有可能无限期地推迟退出的线程,则将这些线程标记为“守护进程”,即使这些线程仍在运行,也允许您的程序完成。

一些您可能感兴趣的参考资料:


2
这是错误的。Python线程是操作系统线程。GIL确实会影响多核系统的性能,但在某些情况下,仍然可以从多个核心中获得一些好处(特别是当线程大部分时间都在C库函数中占用CPU时)。 - Duncan
1
我们可以对此进行争论。事实是:(c)Python仅出于方便之考虑使用底层线程机制(为什么要重新实现已经被证明好用的东西?),但拒绝了“真正”的操作系统级线程所提供的好处。因此,a)是的,它们在技术上是操作系统级线程,但b)不是操作系统级线程,因为除了操作系统级线程的共享内存外,您没有获得任何其他好处。如果您有并行性以提高性能的想法,我甚至建议不要使用它们。 - Don Question
我同意。由于我对GIL和线程的沮丧,我可能会更频繁地使用多进程模块,因为它可以绕过GIL的限制。我建议任何考虑使用多线程来提高性能的人都要考虑这个。 - Drahkar
@DonQuestion 是的,使用过多的线程会增加上下文切换和锁定需求,从而降低应用程序的速度。这对于任何线程系统和任何编程语言都是适用的,而不仅仅是 Python 独有的问题。 - Duncan
2
@Duncan:我强烈推荐看一下David Beazley的精彩演讲。由于天真,我尝试使用线程模块,但是不合逻辑、不一致的性能结果让我感到沮丧。而且这些明显是由Python引起的。在多核、多处理器系统上,如果只使用一个额外的线程,你绝对不会期望获得更差的性能。这是已经证实的事实,是Python的问题。正如Python线程本来就不是为了提高性能而存在,而是为了提高效用。这很好! - Don Question
显示剩余2条评论

0

有很多关于这个问题的好回答,但我觉得守护线程问题仍然没有简单明了地解释。因此,这个答案只涉及到第三个问题

"当所有非守护线程都终止时,主线程将终止."

那么Python守护线程如果"只剩守护线程"时"整个Python程序退出",那么它们就不是Python程序的一部分吗?

如果你想想守护进程是什么,通常它是一个服务。一些代码在无限循环中运行,处理请求,填充队列,接受连接等等。其他线程使用它。在独立进程中就没有任何用处。

因此,程序不能等待守护线程终止,因为它可能永远不会发生。当所有的非守护线程完成时,Python将结束程序。它也会停止守护线程

要等待守护线程完成其工作,请使用join()方法。daemon_thread.join()将使Python在退出之前等待守护线程。 join()还接受一个超时参数。


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