You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 03:37:37