Daemon is not daemon, but what is it?

"daemon thread" 是一个困扰了我很久的概念。官方文档是这么说的:

A thread can be flagged as a “daemon thread”. The significance of this flag is that the entire Python program exits when only daemon threads are left.

然而文档并没有解释这个概念是怎么来的。比如,为什么我在操作系统课上没听过呢?下面是一番搜索之后的结果。

历史

Daemon(守护进程)

daemon,或者 daemon process,在计算机领域里一般指一种在后台执行的程序。这里有一个简单明了的解释:

"Daemon (Daemon Process)" is a notion in UNIX denoting a process detached from any controlling terminal, typically waiting for some event to occur and to respond to in some way. Windows services are similar but I assume Bil et alia chose deliberately a different word for them.

常见的守护进程就是 Linux 里的那一堆 “xxxd” 程序。下文中为避免歧义,称这里的 daemon 为守护进程。

顺便说一句,Python 有专门的 PEP 和库(python-daemon)来实现守护进程。但这并非本文重点,而且我也不懂╮(╯_╰)╭

daemon thread 和守护进程没什么卵关系

是的,他们主要的共同点就是都包含单词“daemon”。但非要说一点联系没有也是不对的。一般而言,被 flag 为 daemon 的线程需要长期在后台执行(比如发送心跳包、检查未读消息等),并且不需和用户直接交互,和守护进程类似。

Python 中的 daemon thread 来自 Java

Google “daemon thread”,第一页全是 Java。我觉得很奇怪,于是找到了 threading.py第一次 commit,前两行赫然写着:

# threading.py:
# Proposed new threading module, emulating a subset of Java's threading model

当初看 concurrent.futures 源码的时候我还在想“直接照搬 Java 的 API 真的好么”,没想到 Guido 居然在 98 年就叛变革命了……

既然 threading.py 是抄来的,daemon 的概念自然也是,之前 Python 的线程 API thread 里可是没有 daemon 的。顺便说一句现在 thread module 变成了 _thread,功能还是一样,对操作系统的线程作了最基本的封装。

Java 文档如是说:

Every thread has a priority. Threads with higher priority are executed in preference to threads with lower priority. Each thread may or may not also be marked as a daemon. When code running in some thread creates a new Thread object, the new thread has its priority initially set equal to the priority of the creating thread, and is a daemon thread if and only if the creating thread is a daemon.

When a Java Virtual Machine starts up, there is usually a single non-daemon thread (which typically calls the method named main of some designated class). The Java Virtual Machine continues to execute threads until either of the following occurs:
* The exit method of class Runtime has been called and the security manager has permitted the exit operation to take place. * All threads that are not daemon threads have died, either by returning from the call to the run method or by throwing an exception that propagates beyond the run method.

嗯,基本一个意思。

应用

Daemon thread 的特性很容易验证,不细说。

import threading
import os
import time

def func():
    time.sleep(5)
    print("finish")

threading.Thread(target=func).start()
threading.Thread(target=func, daemon=True).start()
print("aaa")

更有意义的问题是,为什么 Java/Python 要引入 daemon thread,有什么用处?好在已经有人解释过了

Some threads do background tasks, like sending keepalive packets, or performing periodic garbage collection, or whatever. These are only useful when the main program is running, and it's okay to kill them off once the other, non-daemon, threads have exited.

Without daemon threads, you'd have to keep track of them, and tell them to exit, before your program can completely quit. By setting them as daemon threads, you can let them run and forget about them, and when your program quits, any daemon threads are killed automatically.

简单来说就是,本来并没有 daemon thread,为了简化程序员的工作,让他们不用去记录和管理那些后台线程,创造了一个 daemon thread 的概念。这个概念唯一的作用就是,当你把一个线程设置为 daemon,它会随主线程的退出而退出。关键点有三个:

  1. background task
  2. only useful when the main program is running
  3. ok to kill

被设置为 daemon 的线程应当满足这三条。第一点需要说一下,比如一个线程需要用 join 执行,那么 daemon 就没有意义了,因为程序总是需要等待它完成才能继续执行。

Daemon process

前面说到 Python 里的 daemon 概念抄自 Java,但 Guido 并未止步于此,他把 daemon 的概念推广到了多进程。当用 multiprocessing 创建新的进程时,也可以设置 daemon 属性。

Python 中有很多创建新进程的方法,并非都可以设置为 daemon process。下面列举一些:

multiprocessing.Process:可以设置 daemon
os.system:不可设置 daemon,因为该命令会阻塞当前程序的执行,不满足“background task”
subprocess.Popen:不可设置 daemon,因为 Popen 打开的是外部程序,不满足“only useful when the main program is running”
concurrent.futures.ProcessPoolExecutorworker process 默认是 daemon

Daemon thread 的实现

最后来看一下 daemon thread 的实现,其实很简单。使用最初始(第一次commit)的 threading.py 来分析。

首先看 _MainThread 这个类,它表示主线程,在 import threading 的时候会初始化一个实例__init__ 函数里有这么一句

_sys.exitfunc = self.__exitfunc

_sys 就是 syssys.exitfunc 已经被 atexit 替代,作用都是在程序退出的时候执行清理操作。那么 __exitfunc 是什么呢?

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 self.__oldexitfunc:
        if __debug__:
            self._note("%s: calling exit handler", self)
        self.__oldexitfunc()
    if __debug__:
        self._note("%s: exiting", self)
    self._Thread__delete()

首先调用基类 Thread 中的 __stop 函数。下面是关键:

  1. 首先,t = _pickSomeNonDaemonThread() 顾名思义返回一个 non-daemon 线程。_pickSomeNonDaemonThread 其实就是遍历两个保存了已创建和创建中线程的字典,并检查其 daemon 属性,如果是 non-daemon 则返回。
  2. 这三句是个人都知道在干嘛吧

    while t:
      t.join()
      t = _pickSomeNonDaemonThread()
    
  3. 于是所有 non-daemon threads 都执行完了,主线程退出。

周六的咖啡当与犯人共饮

昨天看了一个短篇,很喜欢。名字叫做《周六的咖啡当与犯人共饮》

短发妹女主是学校推理会的成员,一个狂热的推理迷。(这个短篇里的人全都没有名字……)

结果下一个周六跟踪的时候被长发女生发现了。

长发女生一语道破短发妹跟踪的动机,并且模仿推理小说里的“挑战读者”,把自己换 50 元硬币的动机当做谜题来挑战短发妹。

然而,某次会面时,谜底揭晓。

我无比理解女主此时的心情哈:被泄底+被告知谜题一开始就不存在,绝对是对推理迷的致命打击。

推理只能就此结束。

但是百合漫怎么可能就这么结束呢!!后面的图不放了,再放就是真的泄底了。

这个短篇的阅读经历是非常愉快的,首先是题材新,故事不落俗套,我又完全能理解女主的心情。加上意犹未尽的结尾,对短篇来说可以算满分了。

以前没看过这个作者的漫画,于是就去找了一下:http://manhua.dmzj.com/tags/mofan.shtml,风格比较类似,但果然还是这篇最好。

发布 zhihu-card 0.2.0

zhihu-card 是我最近写的一个东西。受 github-cards 启发,既然知乎的影响越来越大,张兆杰们都把知乎的赞数粉丝数作为体现自身价值的筹码,为什么程序员们不能在自己的网站上展现知乎账户呢?于是就有了这么一个东西。

现在只要在页面中插入两行代码,填入你的 user_hash ,就能显示类似本站右侧的卡片。

<div class="zhihu-card" data-userhash="your hash"></div>
<script src="//cdn.jsdelivr.net/zhihu-card/latest/widget.js"></script>

如果你有兴趣一起来改进,欢迎给项目提 issue 和 pr。

实现

鉴于之前有人问过实现原理,这里简单说一下。显然,如果直接去拿知乎的信息会遇到跨域问题,知乎不允许我们这么干。唯一的解决方法就是用一个 server 去获取用户信息,然后 zhihu-card 去访问 server。server 是拿 Go 写的,目前不开源,因为之后打算大改。

Enjoy.


top