每次按下Ctrl+C强制终止程序时,你是不是也提心吊胆?担心文件没保存、数据库连接没关闭、或者数据状态不一致?这就像突然拔掉正在写入的U盘,轻则数据丢失,重则系统崩溃。我见过太多因为粗暴处理中断导致生产事故的案例——未提交的数据库事务、半截的日志文件、残留的临时锁,这些都是血泪教训。
Python的KeyboardInterrupt异常本质是操作系统SIGINT信号的封装。当你在终端按下Ctrl+C时,系统不是直接杀死进程,而是礼貌地发送一个中断请求。这时候如果程序直接崩溃,相当于客人敲门时你从窗户跳楼逃跑。正确的做法应该是像管家一样,先保存工作进度、收拾好房间,再从容开门应对。
先看这段新手常见的错误示范:
python复制while True:
data = process_data() # 长时间处理
save_to_file(data) # 突然中断会导致文件损坏
改进后的基础版本:
python复制try:
while True:
data = process_data()
save_to_file(data)
except KeyboardInterrupt:
print("\n检测到中断,正在保存最后一批数据...")
emergency_save(data)
print("安全退出")
但这样还不够完美。去年我们有个数据分析脚本就因为这种处理方式,导致最后一批数据重复写入。更专业的做法应该是在循环内设置检查点:
python复制try:
while True:
data = get_stream_data()
with open('output.json', 'a') as f: # 使用追加模式
json.dump(data, f)
time.sleep(1)
except KeyboardInterrupt:
print("\n写入文件句柄已自动关闭")
当你的程序有后台线程时,事情会变得复杂。测试下面这个场景:
python复制def worker():
while True:
print("Working...")
time.sleep(0.5)
try:
threading.Thread(target=worker, daemon=True).start()
time.sleep(10) # 主线程等待
except KeyboardInterrupt:
print("主线程退出") # 但worker线程还在运行!
解决方案是使用事件标志:
python复制exit_event = threading.Event()
def worker():
while not exit_event.is_set():
print("Working...")
time.sleep(0.5)
try:
threading.Thread(target=worker).start()
time.sleep(10)
except KeyboardInterrupt:
exit_event.set()
print("所有线程安全停止")
signal模块可以让你更底层地处理中断信号。这个例子展示了如何实现延时退出:
python复制import signal
class GracefulExiter:
def __init__(self):
self.shutdown = False
signal.signal(signal.SIGINT, self.exit_gracefully)
signal.signal(signal.SIGTERM, self.exit_gracefully)
def exit_gracefully(self, signum, frame):
print(f"\n接收到信号{signum}, 准备优雅退出...")
self.shutdown = True
exiter = GracefulExiter()
while not exiter.shutdown:
data = fetch_data()
process(data)
print("资源清理完成")
结合with语句和上下文管理器,可以构建更安全的资源管理:
python复制class DatabaseConnection:
def __enter__(self):
self.conn = connect_db()
return self.conn
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is KeyboardInterrupt:
print("中断时提交当前事务")
self.conn.commit()
self.conn.close()
return True # 抑制异常
# 使用示例
try:
with DatabaseConnection() as db:
while True:
db.execute(long_running_query)
except KeyboardInterrupt:
print("已确保数据库连接关闭")
对于需要恢复现场的应用,可以参考这个模板:
python复制import pickle
class TaskState:
def __init__(self):
self.checkpoint = 0
self.results = []
def save(self):
with open('state.pkl', 'wb') as f:
pickle.dump(self.__dict__, f)
def load_state():
try:
with open('state.pkl', 'rb') as f:
return pickle.load(f)
except FileNotFoundError:
return {'checkpoint': 0, 'results': []}
state = TaskState()
try:
for i in range(state.checkpoint, 100000):
result = expensive_computation(i)
state.results.append(result)
state.checkpoint = i + 1
if i % 100 == 0:
state.save() # 定期保存
except KeyboardInterrupt:
state.save()
print(f"进度已保存到第{state.checkpoint}项")
在K8s环境中,还需要处理SIGTERM信号:
python复制def handle_shutdown(signum, frame):
logging.info("开始关闭流程")
service.deregister() # 从服务发现注销
queue.stop_accepting() # 停止接收新任务
while not queue.empty(): # 处理剩余任务
process(queue.get())
db.disconnect()
sys.exit(0)
signal.signal(signal.SIGINT, handle_shutdown)
signal.signal(signal.SIGTERM, handle_shutdown)
处理文件IO时可能会遇到这种情况:
python复制try:
with open('/dev/device', 'rb') as f:
data = f.read() # 这里可能卡住不响应中断
except KeyboardInterrupt:
print("实际上不会立即触发")
解决方案是使用带超时的操作:
python复制import fcntl
fd = os.open('/dev/device', os.O_RDONLY | os.O_NONBLOCK)
try:
data = os.read(fd, 1024)
except BlockingIOError:
pass # 处理非阻塞状态
finally:
os.close(fd)
当你的Python程序启动了子进程时:
python复制proc = subprocess.Popen(['ffmpeg', '-i', 'input.mp4'])
try:
proc.wait()
except KeyboardInterrupt:
proc.terminate() # 先尝试友好终止
time.sleep(1)
if proc.poll() is None:
proc.kill() # 强制杀死
print("视频转码已中止")
建议使用这个测试装饰器来模拟中断:
python复制def test_interrupt(func):
def wrapper(*args, **kwargs):
original = signal.signal(signal.SIGINT, lambda *_: None)
try:
thread = threading.Thread(target=func, args=args, kwargs=kwargs)
thread.start()
time.sleep(1) # 让目标函数运行一会
thread._Thread__stop() # 模拟中断
thread.join()
finally:
signal.signal(signal.SIGINT, original)
return wrapper
@test_interrupt
def test_database_cleanup():
db = connect_db()
try:
while True:
db.write(test_data)
except KeyboardInterrupt:
assert db.connection.closed # 验证连接已关闭
记住,好的中断处理就像飞机的紧急滑梯——希望永远用不上,但必须随时可用。在我的爬虫项目中,正是完善的退出机制让程序在凌晨3点被意外中断后,第二天能从中断点继续工作,避免了12小时的重跑成本。