Python 中优雅处理 Ctrl+C 中断:确保数据不丢失的完整方案

凌晨两点半
凌晨两点半
发布时间:2026-10-13| 6人浏览过
原创

本文详解如何在 Python 中通过 signal 模块接管 Ctrl+C 键盘中断信号。针对从 Redis 读取并批量写入 MongoDB 的场景,演示如何定义自定义回调函数,在程序终止前将缓冲区剩余数据持久化到数据库,避免数据丢失。包含完整代码示例及 C 语言扩展下的行为差异说明。

Python 信号处理与数据持久化方案

假设有一个业务场景:需要持续从 Redis 队列中拉取数据,并将其写入 MongoDB 数据库。初学者通常会采用如下基础实现方式:


import json 
import redis 
import pymongo 
client = redis.Redis() 
handler = pymongo.MongoClient().example.col 
while True: 
  data_raw = client.blpop('data', timeout=300) 
  if not data_raw: 
    continue 
  data = json.loads(data_raw[1].decode()) 
  handler.insert_one(data) 

然而,这种逐条插入的方式存在显著性能瓶颈。每条数据都会触发一次独立的 MongoDB 连接操作,导致大量时间耗费在网络 I/O 上,效率极低。

为了解决性能问题,常见的优化策略是引入缓冲区,将代码重构为以下形式:


import json 
import redis 
import pymongo 
client = redis.Redis() 
handler = pymongo.MongoClient().example.col 
to_be_insert = [] 
while True: 
  data_raw = client.blpop('data', timeout=300) 
  if not data_raw: 
    continue 
  data = json.loads(data_raw[1].decode()) 
  to_be_insert.append(data) 
  if len(to_be_insert) >= 1000: 
    handler.insert_many(to_be_insert) 
    to_be_insert = [] 

具体做法是累积数据,当缓冲区中的数据量达到 1000 条时,执行一次批量写入操作至 MongoDB。

但这种实现引入了新的风险:假设在程序运行期间需要更新或重启,此时按下 Ctrl+C 强制终止程序。如果此时缓冲区 `to_be_insert` 中尚有 999 条数据,这些记录将从 Redis 中被移除,却未能写入 MongoDB,导致永久丢失。

理想的行为是:当用户按下 Ctrl+C 时,程序停止从 Redis 拉取新数据,但必须先将缓冲区 `to_be_insert` 中的所有剩余数据(无论数量多少)完整写入 MongoDB,随后再安全关闭程序。

要实现这一需求,关键在于让程序在接收到 Ctrl+C 指令后,仍能执行一段清理代码。然而,默认情况下,Ctrl+C 会直接终止进程,因此需要干预这一默认行为。

SIGINT 信号机制与自定义回调

实际上,按下 Ctrl+C 时,Python 解释器会接收到一个名为 `SIGINT` 的系统信号。根据官方文档定义,收到信号后 Python 会调用信号处理函数。默认的处理逻辑是抛出 `KeyboardInterrupt` 异常并终止程序。我们可以通过注册自定义的信号处理函数来覆盖这一默认行为。

使用信号机制需要导入 Python 内置的 `signal` 库。接着,我们需要定义一个信号回调函数,当 Python 接收到特定信号时,该函数将被调用。

基于上述原理,我们对之前的代码进行如下修改:


import signal 
import json 
import redis 
import pymongo 
 
 
client = redis.Redis() 
handler = pymongo.MongoClient().example.col 
stop = False 
 
 
def keyboard_handler(signum, frame): 
  global stop 
  stop = True 
 
 
signal.signal(signal.SIGINT, keyboard_handler) 
 
to_be_insert = [] 
while not stop: 
  data_raw = client.blpop('data', timeout=300) 
  if not data_raw: 
    continue 
  data = json.loads(data_raw[1].decode()) 
  to_be_insert.append(data) 
  if len(to_be_insert) >= 1000: 
    handler.insert_many(to_be_insert) 
    to_be_insert = [] 
 
if to_be_insert: 
  handler.insert_many(to_be_insert) 

在此代码中,我们定义了一个全局变量 `stop`,初始值为 `False`。因此,只要 `stop` 为假,`while not stop` 循环就会持续运行。

我们定义了一个名为 `keyboard_handler` 的函数,其作用是将全局变量 `stop` 修改为 `True`。需要注意的是,在函数内部修改全局变量时,必须使用 `global` 关键字声明该变量,否则解释器会将其视为局部变量,导致修改无效。

当 `stop` 被修改为 `True` 后,`while not stop` 循环条件不再满足,循环终止,程序流程进入后续代码:


if to_be_insert: 
  handler.insert_many(to_be_insert) 

这段代码检查列表是否包含数据,若有,则执行批量插入 MongoDB 的操作,随后程序正常结束。

整段代码的核心在于 `signal.signal(signal.SIGINT, keyboard_handler)` 这一行,它将 `SIGINT` 信号与 `keyboard_handler` 函数绑定。这意味着在程序运行期间的任何时刻,一旦按下 Ctrl+C,程序会立即跳转到 `keyboard_handler` 函数执行。执行完毕后,程序会返回到之前中断的位置继续执行剩余代码。由于我们在回调函数中已将 `stop` 设为 `True`,主循环不会再次执行,从而直接进入最后的收尾逻辑。

纯 Python 与 C 扩展下的行为差异

需要注意的是,如果整个程序完全由 Python 编写,`signal` 可以在程序执行的任何阶段被触发。只要按下 Ctrl+C,系统会立即进入预设的信号回调函数。

然而,如果程序中混合了 C 语言编写的扩展代码,行为可能有所不同。当按下 Ctrl+C 时,Python 可能需要等待当前正在执行的 C 语言代码执行完毕后,才会响应并进入预设的信号回调函数。这可能导致一定的延迟。

总结

以上介绍了在 Python 中通过信号处理机制接管键盘中断(Ctrl+C)的方法,旨在帮助开发者实现数据的优雅保存与程序的安全退出。

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。