如何在C语言中干净终止线程?树莓派多线程应用技术问询
Hey,针对你在树莓派Raspbian下的C多线程终止问题,我给你梳理一套干净可靠的方案——毕竟线程终止最忌讳粗暴 kill,得结合每个线程的工作逻辑设计协作式退出,同时做好资源清理,避免留下GPIO占用、文件句柄泄漏之类的烂摊子。
核心思路:协作式终止标志 + 针对性唤醒/清理
我们用全局共享的终止标志作为主要触发信号(协作式,最安全),再针对每个线程的阻塞场景做额外处理,最后主线程统一等待所有线程退出。
1. 定义全局终止标志(带互斥锁保护)
首先定义一个线程可见的终止标志,用volatile防止编译器优化,加互斥锁避免竞态:
#include <pthread.h> // 终止标志:1表示所有线程需要退出 volatile int g_terminate = 0; // 保护标志的互斥锁 pthread_mutex_t g_terminate_mutex = PTHREAD_MUTEX_INITIALIZER;
2. 给每个线程添加退出逻辑
线程1:PIR监测+拍照(task1)
这个线程应该是循环轮询GPIO,在每次循环开头检查终止标志,退出前清理摄像头、GPIO资源:
void cleanup_task1() { // 自定义清理逻辑:关闭摄像头句柄、还原GPIO方向等 close_camera_handle(); reset_pir_gpio(); } void *task1(void *arg) { while (1) { // 检查终止标志 pthread_mutex_lock(&g_terminate_mutex); int need_exit = g_terminate; pthread_mutex_unlock(&g_terminate_mutex); if (need_exit) { cleanup_task1(); pthread_exit(NULL); } // 正常逻辑:监测PIR并拍照 if (pir_sensor_triggered()) { capture_photo(); } // 短休眠减少CPU占用 usleep(100000); // 100ms } }
线程2:sigwait+alarm测温度(task2)
这个线程会阻塞在sigwait上,所以除了检查终止标志,还要给它发一个自定义信号唤醒阻塞,同时退出前要取消alarm:
// 提前定义信号集,包含SIGALRM和自定义唤醒信号SIGUSR1 sigset_t g_temp_sig_set; void cleanup_task2() { // 清理:取消当前alarm、关闭温度传感器 alarm(0); close_temp_sensor(); } void *task2(void *arg) { int signum; int interval = *(int*)arg; // 传入的温度测量间隔(秒) while (1) { // 检查终止标志 pthread_mutex_lock(&g_terminate_mutex); int need_exit = g_terminate; pthread_mutex_unlock(&g_terminate_mutex); if (need_exit) { cleanup_task2(); pthread_exit(NULL); } // 设置定时alarm alarm(interval); // 等待信号(SIGALRM触发测温,SIGUSR1用于唤醒退出) sigwait(&g_temp_sig_set, &signum); if (signum == SIGALRM) { // 执行测温逻辑 read_and_log_temperature(); } } }
线程3:检查新照片(task3)
这个线程是循环检查照片目录/队列,同样在循环开头检查终止标志,退出前清理监视器资源:
void cleanup_task3() { // 清理:关闭文件监视器、释放照片队列内存等 stop_photo_monitor(); free_photo_queue(); } void *task3(void *arg) { while (1) { // 检查终止标志 pthread_mutex_lock(&g_terminate_mutex); int need_exit = g_terminate; pthread_mutex_unlock(&g_terminate_mutex); if (need_exit) { cleanup_task3(); pthread_exit(NULL); } // 检查并处理新照片 if (has_new_photo()) { process_photo(); } usleep(500000); // 500ms检查一次 } }
3. 主线程的终止触发与等待逻辑
主线程负责监听系统信号(比如Ctrl+C的SIGINT、系统终止的SIGTERM),触发终止标志,唤醒阻塞的线程,最后等待所有线程退出:
// 信号处理函数:触发终止标志 void handle_terminate_signal(int signum) { pthread_mutex_lock(&g_terminate_mutex); g_terminate = 1; pthread_mutex_unlock(&g_terminate_mutex); } int main() { // 注册信号处理:捕获Ctrl+C和系统终止信号 signal(SIGINT, handle_terminate_signal); signal(SIGTERM, handle_terminate_signal); // 初始化线程2的信号集,并在主线程阻塞这些信号(确保sigwait能收到) sigemptyset(&g_temp_sig_set); sigaddset(&g_temp_sig_set, SIGALRM); sigaddset(&g_temp_sig_set, SIGUSR1); pthread_sigmask(SIG_BLOCK, &g_temp_sig_set, NULL); // 创建线程 pthread_t t1, t2, t3; int temp_interval = 10; // 测温间隔10秒 pthread_create(&t1, NULL, task1, NULL); pthread_create(&t2, NULL, task2, &temp_interval); pthread_create(&t3, NULL, task3, NULL); // 主线程等待终止标志被触发 while (1) { pthread_mutex_lock(&g_terminate_mutex); int need_exit = g_terminate; pthread_mutex_unlock(&g_terminate_mutex); if (need_exit) break; sleep(1); } // 唤醒线程2的sigwait阻塞 pthread_kill(t2, SIGUSR1); // 等待所有线程退出 pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); // 清理全局资源 pthread_mutex_destroy(&g_terminate_mutex); printf("所有线程已干净终止,程序退出\n"); return 0; }
4. 特殊场景的 fallback:线程取消
如果某个线程存在长时间阻塞(比如线程1用了阻塞式GPIO读取,无法轮询标志),可以用pthread_cancel作为补充,但要注意:
- 设置线程为可取消状态,并用
pthread_cleanup_push/pop注册清理函数:
void *task1_blocking(void *arg) { pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); // 延迟取消,在系统调用时响应 pthread_cleanup_push(cleanup_task1, NULL); while (1) { // 阻塞读取PIR信号 if (blocking_read_pir() == 1) { capture_photo(); } } pthread_cleanup_pop(1); // 退出时执行清理函数 pthread_exit(NULL); }
主线程触发终止时,除了设置标志,还可以调用pthread_cancel(t1);,然后pthread_join等待退出。
关键注意事项
- 资源清理优先:每个线程必须在退出前关闭打开的设备句柄、还原GPIO状态、释放动态内存,避免树莓派下次运行时出现资源占用问题。
- 互斥锁必须正确使用:访问共享终止标志时一定要加锁,防止线程间的竞态条件。
- sigwait的信号掩码:主线程必须阻塞sigwait需要的信号,否则信号会被主线程的信号处理函数截获,线程2收不到。
内容的提问来源于stack exchange,提问作者roschach
相关产品推荐
相关产品推荐

