【django】定制 django - 运行你的独立的不死线程

本文详细介绍了如何在Django框架中定制不死线程,通过在子进程中启动独立运行的线程,实现后台任务的持续执行。文章提供了两种判断当前进程是否为子进程的方法,包括仅适用于LINUX平台和通用平台的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【django】定制 django - 运行你的独立的不死线程

2019/May/14 更新 solution

最近开始了解 flask 框架,手写了一点点 flask 代码。用上了 “flask-script” 库之后,flask 的启动方式就极其像开发时的 django。
也发现其监听文件改动重启服务器的功能,这让我原本在 flask 上运行的一个不死线程启动了两次,于是又使用了本文的 solution。实际上本文原先的 solution 本身也是非常好用的,非常有效。

不过在我多写了一些后台代码之后,多理解了一些细节之后,我觉得可以使用现在的新版 solution,或许可能更简单一些,更少一些 trick,更直接一点点。

同样以 django 框架为例。

注,如果是上线的代码/后台服务,是不会启动两个进程的(一个“manage”,一个做服务器),可以根据本次更新的思路很容易就能启动一个后台线程。

django-admin startproject <project name> 后的原始 manage.py 代码:

#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'undeadthread.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc

    execute_from_command_line(sys.argv)


if __name__ == '__main__':
    main()

因为在开发时是通过执行 python manage.py runserver,即 manage.py 是入口处,所以在 manage.py 文件中增加一点点代码:

#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
import requests
from time import sleep

def enable_undeadthread():
    cnt = 10
    while cnt:
        try:
            requests.get("http://localhost:8000/undeadthread", )
        except Exception:
            cnt -= 1
            sleep(1)
        else:
            break

def main():
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'undeadthread.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "...略..."
        ) from exc

    import threading
    threading.Thread(target=enable_undeadthread, daemon=True).start()

    execute_from_command_line(sys.argv)

if __name__ == '__main__':
    main()

可以看到,这里需要 requests 库,使用 python 标准库的 urllib 之类的也可以实现相同的作用,但是使用 requests 库代码可以更简单一些。

上面请求的 url 就是这个 manage.py runserver 启动的 server(你可以根据我旧版 solution 另一篇博客 ? 【django】(定制 django)启动服务器时调用默认浏览器打开项目首页 解析一下命令行,这样就能控制 domain 和 端口了)

接下来添加一下请求的 url 中的 /undeadthread 部分(在最顶层 urls.py 中添加):

from django.contrib import admin
from django.urls import path
import sys
from django.http import HttpResponse

ONE_UNDEAD_THREAD_FLAG = False

def enable_undeadthread(request):
    import threading
    threading.Thread(target=undeadthread, daemon=True).start()
    return HttpResponse("it works")

def undeadthread():
    global ONE_UNDEAD_THREAD_FLAG

    from time import sleep, ctime

    if not ONE_UNDEAD_THREAD_FLAG:
        ONE_UNDEAD_THREAD_FLAG = True
    else:
        return

    while True:
        print(ctime(), file=sys.stderr)
        sleep(2)

urlpatterns = [
    path('admin/', admin.site.urls),
]

urlpatterns += [
    path('undeadthread', enable_undeadthread),
]

可以看到,def undeadthread() 就函数就是会从服务器从开始到结束一直运行的线程。

当然,除非它自己内部出错又没有处理,提前挂掉。

undeadthread 函数内部的 while True 编写定制的代码即可。


这次更新的原因主要是对“异步”理解稍微更深一些,

所以想到请求一个 url 时,服务器内部开始运行一个函数(在 django 框架中写代码,这个函数一般在 views.py 中,这里为了简单起见直接写在 urls.py 中),然后函数可以启动任意的线程或进程,至于启动的线程或进程退不退出其实和这个处理 request 的函数已经没有关系了(除非它等待线程或进程结束,不过那样就不是异步了,最多是并发或着并行)。所以,该函数的任务可以是启动一个线程在后台运行,然后自己响应(response) request,即退出。

但是被启动线程只要还没有 returrn 掉(和没有抛出异常),那么该线程就会继续运行。

=== END 更新


==== 旧版 solution 分割线 ===


说在前面的话

通过《轻量级 django》应该也可以定制 django,而且可能更“灵活”。
并且可能会比本篇介绍的方法更加“健壮”。

不过本篇的方法比较“简单”一点,并且暂时不用像《轻量级 django》中对 django 的框架机制了解地那么清楚。因此本篇还是值得一看的。


Q: 实话说,你们为什么要如此热衷于使用 django,即使在它上面跑个 while 循环都不太那么“明白”地实现情况下,还要对它一改再改?

A: 因为 django 的 ORM 很棒呀!!!

准备工作

判断当前进程是否是子进程

启动 django (manage.py runserver)最后会有两个进程在主机中运行。

父进程
父进程一个在开发阶段最重要的功能就是 “监听文件改动,如果有文件改动,则重启子进程”;当然,django 父进程的功能还有其它用途,但是本人没有对其进一步研究,所以就不多言了。

子进程
子进程提供 HTTP server 功能。

我们将需要在 django 中使用的线程放在子进程中,因为这样可以和子进程共享内存空间(变量等)。

所以需要判断当前进程是否为子进程,以用来启动我们的定制线程,在父进程中启动定制线程一般都没有意义。
(当然,如果你需要在父进程中启动,那就做相反的操作)

仅 LINUX 平台可用的 solution

(MAC OS 平台未测试)

def Is_child_processing():
    import re
    re_result = re.findall(
        "{}".format(os.getpid()),
        os.popen("ps -ejf | grep {} | grep -v \"grep\"".format(os.getpid())).read()
        )
    ret = True if len(re_result) == 1 else False
    return ret

在 Terminal 中输入 $ ps -ejf | grep {} | grep -v "grep" 查看输出,了解这段代码是如何实现的原理。

感觉这段代码应该没有必要做解释 ?。

通用平台的 solution
def Is_child_processing():
    from multiprocessing.connection import Listener
    from queue import Queue

    q = Queue()

    def lock_system_port(_port):
        nonlocal q  # it's OK without this announce line 
        try:
            listener = Listener(("", _port))
            q.put(False)
        except Exception:  # port be used by parent
            # traceback.print_exc()
            q.put(True)
            return  # child don't listen

        while True:
            serv = listener.accept()  # just bind the port.

    t = threading.Thread(target=lock_system_port, args=(62771, ))
    t.setDaemon(True)
    t.start(); del t;
    return q.get()

当然,正如你所见到的,这种 solution 会占用一个系统端口。

这个 solution 的机制就是通过占用一个系统端口,因为端口只能被一个进程占用(当然,一个进程内也不能重复打开端口),所以子进程运行时,会执行同样的代码,但是因为端口已经被父进程占用了,于是就可以判断当前进程是子进程。

当然,这段代码还有不少线程的设计细节,希望你能明白我为什么要这么设计这段代码。

  1. 线程共享可以访问到的变量,但是线程之间不能直接通信;
  2. 并且父进程中的 lock_system_port 函数不会退出,知道 django server 停止运行,所以不能简单地通过获取这个函数的返回值来判断是否是子进程。

当然,我说的是不能“简单地”,通过一些复杂的超时机制设计,理论上总是可以实现这种方式的。

基于以上两点,使用一个 Queue 来悉知 lock_system_port 函数(该函数被用作独立的线程运行)指示的是否是子进程的状态是必要的。
3. line 18, 19 lock_system_port 函数在父进程中使用 while True 死循环运行是必要的,否则该线程退出,端口就没有被绑定,子进程运行到这段代码,就会拿到这个端口,整个功能就没有起到作用。
4. t.setDaemon(True) 是必要的,不是说这一行代码是必要写的,而是 “Daemon” 线程这个状态为 True 是必要的。否则 Ctrl + C 这类的方式可能无法(一次性)完全退出 server。这是因为多线程程序中,系统会等待所有“ Daemon”线程退出之后,该进程才真正退出- 释放资源。而 “Daemon” 线程被视作“退出状态”是无关紧要的,所以当所有的 “非 Daemon” 线程退出之后,既是还有 “Daemon” 线程 正在运行,该进程也会退出。

Note - 本段非常重要,避免 solution 的使用陷阱

并不是所有一个程序有多个进程的地方都能使用这个 solution,因为父进程和子进程的启动顺序是不确定的,并不是说父进程一定比子进程先运行某一段共同的代码。
但是在 django 的 case 中,这段代码我们总是能预期父进程先运行到的。因此可以使用这个 solution。


直接放代码

默认 startproject 创建的 manage.py look like

#!/usr/bin/env python
import os
import sys

if __name__ == '__main__':
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)

加入运行定制的独立运行的线程

不死线程,会死的线程就没啥意义了,一个线程启动,运行,退出;直接在 views.py 加一个 function 一样能够实现这个功能。

#!/usr/bin/env python
import os
import sys


def customization():
    import threading
    t = threading.Thread(target=_run)
    t.setDaemon(True)
    t.start()
    del t


def _run():
    from time import sleep
    from time import ctime
    while True:  # loop until universe collapses
        print("{} customization > _run is running".format(ctime()), file=sys.stderr)
        sleep(5)


def Is_child_processing():
    [...这个位置的代码参见上文,选择你喜欢的 solution...]


if __name__ == '__main__':
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc

    if Is_child_processing():
        customization()

    execute_from_command_line(sys.argv)

打印输出混在一起?
需要一个单独的输出?

为你献上一版更新,使用 logging module

#!/usr/bin/env python
import os
import sys

def customization():
    import threading
    t = threading.Thread(target=_run)
    t.setDaemon(True)
    t.start()
    del t

def _run():
    from time import sleep
    from time import ctime

    #
    # logging
    #
    import logging
    logging.basicConfig(
        filename=os.path.join(os.getcwd(), "customization_run.log"),
        level=logging.INFO,
        # level=logging.WARNING,
        format='[%(asctime)s]%(levelname)-9s%(message)s',
    )
    while True:  # loop until universe collapses
        logging.info("customization > _run is running")
        sleep(5)

def Is_child_processing():
    [...这个位置的代码参见上文,选择你喜欢的 solution...]

if __name__ == '__main__':
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc

    if Is_child_processing():
        customization()

    execute_from_command_line(sys.argv)

简单的运行说明:

#### terminal 1 ###
<path>/<to>/<django>/<proj>/<dir> $ python manage.py runserver
...django information output...
Ctrl+C to quit

? 这样就和平常启动 django 无异。

现在查看 customization 的 不死线程输出:

#### terminal 2 ####
<path>/<to>/<django>/<proj>/<dir> $ tail -f customization_run.log
.........the log ........
you can use Ctrl+C to quit "tail -f"

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值