技术分享 2025-02-09

Python多线程(2)

Python多线程实战笔记

Python 多线程 监控循环

python多线程实战——监控循环

在项目开发过程中,总结出关于多线程的知识与经验,供开发者参考

监控循环线程

监控循环是多线程最基本的一个应用之一。在特定场景下,如需要感知用户是否离线/触发保活机制时,可以创建一个后台线程,用来监控用户行为,可能的代码如下:

import time  
import threading

def monitor_loop():
    interval = 600 # 每600s监控一次
    while True:
        monitor() # 具体的监控逻辑
        time.sleep(interval)

monitro_thread = threading.thread(target = monitor_loop, daemon = True) # 守护线程  
monitro_thread.start() 

一般来说,我们会设置监控线程为守护线程,因为如果进程结束了,也就没有继续监控的必要了,线程自动退出即可

监控线程控制

如果使用while True,会导致监控线程启动后无法手动关闭,需要进程结束后自动退出,灵活性不足。因此,通常通过监控器类的方式来控制监控线程,并且提供一个启动的标志,如:

import time 
import threading 

class Monitor:
    def __init__(self):
        self._started = False 
        self._lock = threading.Lock()
        self._interval = 600
        self._thread = None

    def start(self, interval=None):
        with self._lock:
            if self._started:
                return
            self._started = True
            if interval is not None:
                self._interval = interval
            self._thread = threading.Thread(target=self._run, daemon=True)
            self._thread.start()

    def stop(self):
        with self._lock:
            self._started = False

    def is_running(self):
        with self._lock:
            return self._started

    def _run(self):
        while self.is_running():
            self.monitor()
            time.sleep(interval)

    def monitor(self):
        # 具体的监控逻辑在此处实现
        print("正在监控...")
通过设置self._startedself.is_running()的方式,我们可以定义线程的运行状态,如果self._started == True,则启动监控循环后,while self.is_running()就可以运行了;而self.stop()通过设置self._started = False,来中断监控循环

由于self.start()self.stop()(通常由主线程或其他线程调用)与监控线程内部(即_run方法)对self._started标志的读取是并发进行的,存在竞态条件。如果没有适当的同步机制,可能出现这样的问题:主线程刚刚设定self._started = False,但监控线程还没看到变化,仍然进入下一轮循环;反之亦然。
为了保证线程安全,必须用锁(如threading.Lock)将所有访问、修改self._started的操作包裹起来,实现原子性。这也是示例代码中with self._lock语句块的作用,确保任意时刻只有一个线程能够读写self._started,从而避免竞态和数据不一致的问题。

监控线程的拟同步停止

拟同步停止指的是,需要调用stop()后立即终止线程,在self.interval很小时,停止几乎是同步的(假设,self.stop()修改self._started时,while循环刚好开始新的一轮监控 + 等待,需要至少self.interval的时间,如果self.interval很小,这个影响忽略不计),但是如果self.interval很大(如600s),在调用self.stop()后,可能需要等待600s,不符合我们同步停止的需求。

为了解决这个问题,可以设计一个拟同步停止的工具,原理为,拆分time.sleep(long_interval)为多次time.sleep(short_interval),每次短间隔等待后,都判断是否需要退出进程,具体实现如下:

"""
可中断的等待工具:在长间隔内按小步长睡眠并定期检查“是否继续运行”标志,
便于关闭线程时快速退出,无需等满整个配置间隔。
"""
import time
from typing import Callable


def interruptible_sleep(
    total_seconds: float,
    keep_running: Callable[[], bool],
    check_interval: float = 1.0,
) -> bool:
    """
    可中断的等待:在 total_seconds 内按 check_interval 分段睡眠,每段后检查 keep_running。
    当 keep_running() 返回 False(表示应停止)时立即返回;否则睡满 total_seconds 后返回。

    用于替代 time.sleep(interval),使工作循环在收到停止信号后能在一个 check_interval 内退出。

    参数
    -----
    total_seconds : float
        计划等待的总时间(秒),对应业务上的间隔,如 config 中的 interval。
    keep_running : Callable[[], bool]
        无参可调用对象,返回 True 表示继续运行、应继续等待,返回 False 表示应停止、立即结束等待。
        例如: lambda: self._monitoring 或 lambda: self.monitor_started。
    check_interval : float, 默认 1.0
        每次睡眠的步长(秒),即检查 keep_running 的频率。不宜过大,否则停止响应仍会变慢。

    返回
    -----
    bool
        True:已睡满 total_seconds(未收到停止信号)。
        False:因 keep_running() 返回 False 而提前结束等待。

    示例
    -----
    >>> # 在监控循环中替代 time.sleep(interval)
    >>> while self._monitoring:
    ...     self._do_work()
    ...     if not interruptible_sleep(600, lambda: self._monitoring, check_interval=1.0):
    ...         break  # 收到停止,退出循环
    """
    if total_seconds <= 0:
        return True
    check_interval = max(0.01, min(check_interval, total_seconds))
    deadline = time.monotonic() + total_seconds
    while True:
        remaining = deadline - time.monotonic()
        if remaining <= 0:
            return True
        chunk = min(check_interval, remaining)
        time.sleep(chunk)
        if not keep_running():
            return False

注意,使用Callable来传递self._started,类似于C中的引用方法,因为self._started是不断变化的,直接传递一个started参数,则工具内部,started一直为False,起不到检测self._started是否结束的作用。

import threading
import time

class Monitor:
    def __init__(self):
        self._started = False
        self._lock = threading.Lock()
        self._interval = 600
        self._thread = None

    def start(self, interval=None):
        with self._lock:
            if self._started:
                return
            self._started = True
            if interval is not None:
                self._interval = interval
            self._thread = threading.Thread(target=self._run, daemon=True)
            self._thread.start()

    def stop(self):
        with self._lock:
            self._started = False

    def is_running(self):
        with self._lock:
            return self._started

    def _run(self):
        while self.is_running():
            self.monitor()
            # 用 interruptible_sleep 替代 time.sleep
            if not interruptible_sleep(self._interval, self.is_running, check_interval=1.0):
                break

    def monitor(self):
        # 具体的监控逻辑在此处实现
        print("正在监控...")