解码线程编程

线程的概念

进程是正在运行的程序,是操作系统分配资源(代码、数据、内存、文件资源等)的基本单位。线程是进程内的执行单元,是操作系统调度的最小单位,一个进程至少包含 1 个主线程(程序默认的 main 函数执行流),也可创建多个子线程。

image

线程与进程的关系类似工厂与工人:进程(工厂)提供所有资源,多个线程(工人)共享这些资源,各自独立执行特定任务,协同完成进程的整体功能。

image

核心特点:

  • 同一进程内的所有线程共享进程资源(如内存空间、文件描述符),也可拥有私有资源(如线程自身的栈空间)。
  • 操作系统调度线程而非进程,线程切换开销远小于进程切换。
  • 主线程若通过 return 或 exit () 终止,整个进程会结束,所有子线程也会随之终止;若主线程调用 pthread_exit (),仅自身终止,子线程可继续运行。

线程的创建

通过pthread_create()函数创建线程,创建成功后线程会立即执行指定的任务函数(线程函数)。

函数原型与核心说明

#include <pthread.h>
/**
 * @brief 创建一个新线程
 * @param thread: 输出参数,指向pthread_t类型变量,用于存储创建成功后的线程ID
 * @param attr: 线程属性指针,NULL表示使用默认属性(可连接状态)
 * @param start_routine: 线程函数指针,线程创建后会自动执行该函数,函数原型必须为void* (*)(void*)
 * @param arg: 传递给线程函数的参数,需强转为void*类型,线程函数内再转回原类型
 * @return int: 成功返回0,失败返回非0错误码(不同错误对应不同错误码,可通过strerror(errno)查看描述)
 * @note 编译时必须加-lpthread选项链接线程库(如gcc test.c -o test -lpthread)
 * @warning 传递给arg的参数若为局部变量,需确保线程访问时该变量未被销毁(建议用全局变量或动态内存)
 */
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);

示例代码(获取触摸屏坐标)

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <linux/input.h>  // 输入子系统事件结构体定义
#include <unistd.h>
#include <pthread.h>      // 线程操作库
#include <errno.h>
#include <string.h>       // 用于strerror等错误处理

// -------------------------- 硬件分辨率配置(务必根据实际设备修改!) --------------------------
#define TS_WIDTH    1024  // 触摸屏原始X轴范围(例:0~1023,通过硬件手册或工具查询)
#define TS_HEIGHT   600   // 触摸屏原始Y轴范围(例:0~599)
#define LCD_WIDTH   800   // 目标LCD屏幕X轴像素范围(例:0~799)
#define LCD_HEIGHT  480   // 目标LCD屏幕Y轴像素范围(例:0~479)

// -------------------------- 全局变量(线程间共享数据) --------------------------
int g_ts_x = 0;    // 触摸屏原始X坐标(子线程写入,主线程读取)
int g_ts_y = 0;    // 触摸屏原始Y坐标
int g_lcd_x = 0;   // 转换后的LCD X坐标
int g_lcd_y = 0;   // 转换后的LCD Y坐标
int g_ts_fd = -1;  // 触摸屏设备文件描述符(主线程打开,子线程使用)

// -------------------------- 函数实现 --------------------------
void ts_to_lcd(int ts_x, int ts_y, int *lcd_x, int *lcd_y) {
    // 空指针校验:避免非法访问导致程序崩溃
    if (lcd_x == NULL || lcd_y == NULL) {
        fprintf(stderr, "[ERROR] ts_to_lcd: 输出参数lcd_x/lcd_y不可为NULL\n");
        return;
    }

    // 核心转换逻辑:线性映射(保持坐标比例一致)
    *lcd_x = ts_x * LCD_WIDTH / TS_WIDTH;
    *lcd_y = ts_y * LCD_HEIGHT / TS_HEIGHT;

    // 边界保护:确保坐标在LCD有效范围内(防止超出屏幕显示区域)
    *lcd_x = (*lcd_x < 0) ? 0 : ((*lcd_x >= LCD_WIDTH) ? (LCD_WIDTH - 1) : *lcd_x);
    *lcd_y = (*lcd_y < 0) ? 0 : ((*lcd_y >= LCD_HEIGHT) ? (LCD_HEIGHT - 1) : *lcd_y);
}

void *ts_read_thread(void *arg) {
    // 设备合法性校验:确保主线程已成功打开触摸屏
    if (g_ts_fd < 0) {
        fprintf(stderr, "[ERROR] ts_read_thread: 触摸屏设备未初始化(fd=%d)\n", g_ts_fd);
        pthread_exit(NULL);  // 线程异常退出
    }

    struct input_event ts_event;  // 存储单个输入事件(触摸、同步等)
    int coord_cnt = 0;            // 坐标计数:累计获取X+Y(满2个表示坐标完整)

    printf("[THREAD] 触摸读取线程启动,等待触摸事件...\n");

    // 死循环:持续读取触摸事件(无事件时阻塞,不占用CPU资源)
    while (1) {
        int ret = read(g_ts_fd, &ts_event, sizeof(ts_event));
        if (ret != sizeof(ts_event)) {  // 读取不完整(设备异常或数据损坏)
            fprintf(stderr, "[ERROR] ts_read_thread: 读取触摸事件失败,原因:%s\n", strerror(errno));
            close(g_ts_fd);  // 关闭设备,避免资源泄漏
            g_ts_fd = -1;    // 标记设备已失效
            pthread_exit(NULL);
        }

        // -------------------------- 解析输入事件类型 --------------------------
        // 绝对坐标事件(EV_ABS):提取X/Y轴坐标
        if (ts_event.type == EV_ABS) {
            if (ts_event.code == ABS_X) {  // X轴坐标事件
                g_ts_x = ts_event.value;   // 更新全局原始X坐标
                coord_cnt++;               // 计数+1
            } else if (ts_event.code == ABS_Y) {  // Y轴坐标事件
                g_ts_y = ts_event.value;   // 更新全局原始Y坐标
                coord_cnt++;               // 计数+1
            }
        }
        // 触摸松开事件(EV_KEY+BTN_TOUCH+value=0):手指离开屏幕时重置计数
        else if (ts_event.type == EV_KEY && ts_event.code == BTN_TOUCH && ts_event.value == 0) {
            coord_cnt = 0;  // 避免下次触摸残留旧计数
        }

        // -------------------------- 坐标完整时执行转换 --------------------------
        if (coord_cnt >= 2) {  // 已获取完整X+Y坐标
            ts_to_lcd(g_ts_x, g_ts_y, &g_lcd_x, &g_lcd_y);  // 转换为LCD坐标
            coord_cnt = 0;  // 重置计数,等待下一次触摸
        }
    }

    // 理论上不会执行到此处(循环为死循环)
    pthread_exit(NULL);
}

// -------------------------- 主函数(程序入口) --------------------------
int main(int argc, char const *argv[]) {
    pthread_t ts_thread_id;  // 触摸读取子线程ID
    g_ts_fd = open("/dev/input/event0", O_RDWR);
    if (g_ts_fd == -1) {
        fprintf(stderr, "[ERROR] main: 打开触摸屏设备失败,原因:%s\n", strerror(errno));
        fprintf(stderr, "[TIP] 1. 确认设备路径是否正确 2. 用sudo运行程序(输入设备需root权限)\n");
        exit(1);  // 程序异常退出
    }
    printf("[MAIN] 触摸屏设备打开成功,设备描述符fd=%d\n", g_ts_fd);

    int ret = pthread_create(&ts_thread_id, NULL, ts_read_thread, NULL);
    if (ret != 0) {
        fprintf(stderr, "[ERROR] main: 创建触摸线程失败,错误码=%d,原因:%s\n", ret, strerror(ret));
        close(g_ts_fd);  // 清理资源
        exit(1);
    }
    printf("[MAIN] 触摸读取子线程创建成功,线程ID=%lu\n", (unsigned long)ts_thread_id);

    // 主线程不参与触摸读取,专注于LCD坐标的应用(如显示到屏幕、数据传输等)
    while (1) {
        printf("[MAIN] 原始坐标=(%4d, %4d) → LCD坐标=(%4d, %4d)\n",
               g_ts_x, g_ts_y, g_lcd_x, g_lcd_y);
        sleep(1);  // 延时1秒,避免打印刷屏(实际项目可替换为LCD显示代码)
    }

    pthread_join(ts_thread_id, NULL);  // 等待子线程退出(回收线程资源)
    close(g_ts_fd);                    // 关闭设备文件(释放fd)
    printf("[MAIN] 程序正常退出\n");
    return 0;
}

编译链接注意事项

  • 如果遇到
    • arm_test.c:(.text+0x3f4): undefined reference to `pthread_create'
      collect2: error: ld returned 1 exit status
    • 编译须加lpthread选项(链接线程库)。
  • 示例编译命令:gcc thread_demo.c -o thread_demo -lpthread

线程的结束

线程结束有多种方式,核心是安全终止线程并避免资源泄漏,常用方式为pthread_exit()函数。

线程结束的三种合法方式

  • 调用pthread_exit(void *retval):主动终止当前线程, retval 为退出状态(供其他线程获取)。
  • 线程函数执行 return:效果等同于在 return 处调用pthread_exit(返回值)
  • 其他线程调用pthread_cancel(pthread_t thread):取消指定线程(需线程支持取消机制)。

关键区别

  • pthread_exit():仅终止当前线程,进程及其他线程不受影响。
  • exit(int status):终止整个进程,所有线程都会被强制终止。
  • 主线程 return:等同于调用exit(return值),终止整个进程。

pthread_exit () 函数详解

#include <pthread.h>
/**
 * @brief 终止当前线程
 * @param retval: 线程退出状态指针,若其他线程调用pthread_join可获取该值
 * @return 无返回值(函数调用后不会回到调用点)
 * @note retval指向的内存不能是线程栈上的局部变量(线程终止后栈空间会释放)
 * @warning 若线程是可连接状态,retval需指向全局变量或动态分配的内存(避免野指针)
 */
void pthread_exit(void *retval);

示例代码(线程主动退出)

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 子线程1:执行一次任务后退出
void *task_1(void *arg) {
    printf("task_1 start\n");
    sleep(2); // 模拟任务执行
    printf("task_1 exit\n");
    // 退出状态返回10(需用(void*)强转,因返回类型为void*)
    pthread_exit((void *)10);
}

// 子线程2:无限循环运行
void *task_2(void *arg) {
    while (1) {
        printf("task_2 running\n");
        sleep(1);
    }
}

int main(int argc, char const *argv[]) {
    pthread_t thread_1, thread_2;

    // 创建线程1
    pthread_create(&thread_1, NULL, task_1, NULL);
    // 创建线程2
    pthread_create(&thread_2, NULL, task_2, NULL);

    // 主线程睡眠3秒后退出(仅终止主线程,线程2继续运行)
    sleep(3);
    printf("main thread exit\n");
    pthread_exit(NULL);

    // 以下代码不会执行
    return 0;
}

线程的接合(回收资源)

线程退出后不会立即释放资源,会变为 “僵尸线程”,需通过pthread_join()函数回收资源并获取退出状态。

pthread_join () 函数详解

#include <pthread.h>
/**
 * @brief 等待指定线程终止,回收其资源并获取退出状态
 * @param thread: 要等待的线程ID(创建线程时通过pthread_create获取)
 * @param retval: 输出参数,用于存储线程的退出状态(对应pthread_exit的retval)
 *                若线程被取消,retval会被设为PTHREAD_CANCELED
 * @return int: 成功返回0,失败返回非0错误码
 * @note 该函数是阻塞函数,若目标线程未终止,调用线程会一直等待
 * @warning 同一线程不能被多个线程同时join,否则结果未定义
 *          若不需要获取退出状态,retval可设为NULL
 */
int pthread_join(pthread_t thread, void **retval);

pthread_tryjoin_np()非阻塞接合

#include <pthread.h>

/**
 * @brief 非阻塞尝试接合目标线程,目标未终止则直接返回错误
 * @param thread: 目标线程的用户态ID(同pthread_join)
 * @param retval: 输出参数,存储退出状态(同pthread_join)
 * @return int: 成功返回0;失败返回错误码(EBUSY:目标线程未终止,EINVAL:不可接合等)
 * @note 1. 后缀np(non-portable):非POSIX标准,Linux特有(需包含<pthread.h>,编译无需额外选项)
 *       2. 适用场景:避免调用线程被长时间阻塞(如主线程需定期处理其他任务,同时检查子线程是否结束)
 */
int pthread_tryjoin_np(pthread_t thread, void **retval);

pthread_timedjoin_np()超时接合

#include <pthread.h>
#include <time.h>  // 需包含时间相关头文件

/**
 * @brief 超时等待接合目标线程,超时后返回错误
 * @param thread: 目标线程的用户态ID(同pthread_join)
 * @param retval: 输出参数,存储退出状态(同pthread_join)
 * @param abstime: 绝对超时时间(struct timespec类型),需设置为“当前时间 + 超时时长”
 * @return int: 成功返回0;失败返回错误码(ETIMEDOUT:超时,EBUSY:未超时但线程未终止等)
 * @note 绝对超时时间:需用clock_gettime()获取当前时间,再加上超时时长(避免系统时间变化导致误差)
 *       适用场景:限制接合的最大等待时间(如主线程最多等待5秒,超时则放弃接合并处理其他逻辑)
 */
int pthread_timedjoin_np(pthread_t thread, void **retval, const struct timespec *abstime);

pthread_self()函数详解

#include <pthread.h>

/**
 * @brief 获取调用该函数的当前线程的用户态ID(TID)
 * @param 无参数
 * @return pthread_t: 当前线程的ID,类型为pthread_t(具体实现可能是结构体指针或整数,不可直接用%d打印)
 * @note 线程ID的作用域:仅在当前进程内有效,不同进程的pthread_t可能重复,不能跨进程使用
 *       与内核态TID的区别:内核态TID通过系统调用gettid()获取(返回int类型,内核全局唯一),二者不可混用
 *       打印方式:Linux下pthread_t通常是unsigned long类型,建议用%lu格式化输出(避免移植性问题)
 *       核心用途:线程自我标识(如自我分离、日志打印线程ID、判断当前线程是否为主线程等)
 */
pthread_t pthread_self(void);

示例代码(回收线程资源并获取退出状态)

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>// 存储主线程ID(供子线程使用)
#include <errno.h>
#include <string.h>
pthread_t main_thread;

/**
 * @brief 子线程任务:等待主线程退出并打印其退出状态
 * @param arg: 无参数(设为NULL)
 * @return void*: 线程退出状态
 */
void *task(void *arg) {
    void *exit_status;

    printf("sub thread waiting for main thread...\n");
    // 阻塞等待主线程终止,获取退出状态
    int ret = pthread_join(main_thread, &exit_status);
    if (ret != 0) {
        fprintf(stderr, "pthread_join error[%d],%s\n", errno,strerror(errno));
        pthread_exit(NULL);
    }

    // 打印主线程退出状态(需转回int类型)
    printf("main thread exit status: %d\n", *((int*)exit_status));
    // 子线程退出
    pthread_exit(NULL);
}

int main(int argc, char const *argv[]) {
    pthread_t sub_thread;

    // 创建子线程
    pthread_create(&sub_thread, NULL, task, NULL);

    // 获取主线程自身ID
    main_thread = pthread_self();
    printf("main thread ID: %lu\n", (unsigned long)main_thread);

    // 主线程睡眠5秒后退出,返回状态100
    sleep(5);
    printf("main thread is exiting...\n");
    // 退出状态用全局变量或动态内存(此处用全局变量地址,避免栈空间释放问题)
    static int status = 100;
    pthread_exit((void *)&status);

    return 0;
}

关键注意事项

  • 若线程未被 join 且未设置分离属性,会一直占用资源(僵尸线程)。
  • 若不需要获取线程退出状态,可将线程设为分离属性(无需 join)。

线程的属性

线程属性通过pthread_attr_t结构体管理,核心属性包括分离状态、栈大小、调度策略等,重点是分离属性(解决僵尸线程问题)。

线程属性的基本操作流程

  • 定义属性对象:pthread_attr_t attr
  • 初始化属性对象:pthread_attr_init(pthread_attr_t *attr)
  • 设置属性(如分离状态):pthread_attr_setdetachstate()
  • 用该属性创建线程:pthread_create()的第二个参数传入属性对象。
  • 销毁属性对象:pthread_attr_destroy(pthread_attr_t *attr)(属性对象不再使用时)。

核心属性:分离状态

分离状态决定线程终止后资源的释放方式:

  • 可连接状态(默认):线程终止后需调用pthread_join()回收资源。
  • 分离状态:线程终止后自动释放资源,无需 join(不能被 join)。

核心属性操作函数

初始化与销毁属性对象

#include <pthread.h>
/**
 * @brief 初始化线程属性对象(设置为默认属性)
 * @param attr: 指向pthread_attr_t类型的指针
 * @return int: 成功返回0,失败返回非0错误码
 * @note 必须先初始化,才能设置其他属性
 */
int pthread_attr_init(pthread_attr_t *attr);

/**
 * @brief 销毁线程属性对象(释放资源)
 * @param attr: 已初始化的属性对象指针
 * @return int: 成功返回0,失败返回非0错误码
 * @note 销毁后属性对象不可再使用,除非重新初始化
 */
int pthread_attr_destroy(pthread_attr_t *attr);

设置分离状态(关键属性)

#include <pthread.h>
/**
 * @brief 设置线程的分离状态(决定线程是否可接合)
 * @param attr: 已初始化的属性对象指针
 * @param detachstate: 分离状态值,可选:
 *                     - PTHREAD_CREATE_DETACHED:分离状态(自动回收资源,不可接合)
 *                     - PTHREAD_CREATE_JOINABLE:可连接状态(默认,需pthread_join回收)
 * @return int: 成功返回0,失败返回非0错误码
 * @note 分离状态的线程终止后,系统自动释放资源,无需其他线程接合
 */
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

/**
 * @brief 获取线程属性对象的分离状态
 * @param attr: 属性对象指针
 * @param detachstate: 输出参数,存储当前分离状态
 * @return int: 成功返回0,失败返回非0错误码
 */
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);

动态设置分离状态(线程创建后)

#include <pthread.h>
/**
 * @brief 动态将目标线程设置为分离状态(线程创建后使用)
 * @param thread: 目标线程的ID
 * @return int: 成功返回0,失败返回非0错误码
 * @note 1. 只能对“可连接状态”的线程调用,已分离线程调用结果未定义
 *       2. 线程自身可通过pthread_self()获取ID,实现自我分离
 */
int pthread_detach(pthread_t thread);

示例:创建时设置分离属性(pthread_attr_setdetachstate)

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
/**
 * @brief 子线程任务:输出线程ID后退出
 * @param arg: 传递的字符串参数
 * @return void*: 退出状态
 */
void *task(void *arg) {
    // 打印传递的参数和自身ID
    printf("%s, my TID: %lu\n", (char *)arg, (unsigned long)pthread_self());
    // 线程退出,资源自动释放(因是分离属性)
    pthread_exit(NULL);
}

int main(int argc, char const *argv[]) {
    // 定义线程属性对象
    pthread_attr_t thread_attr;
    // 始化属性对象(设置为默认属性)
    int ret = pthread_attr_init(&thread_attr);
    if (ret != 0) {
        fprintf(stderr, "pthread_attr_init error[%d]%s\n", errno, strerror(errno));
        exit(1);
    }

    // 置属性为分离状态(PTHREAD_CREATE_DETACHED)
    ret = pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED);
    if (ret != 0) {
        fprintf(stderr, "set detachstate error[%d]%s\n", errno, strerror(errno));
        pthread_attr_destroy(&thread_attr);
        exit(1);
    }

    // 用该属性创建两个子线程
    pthread_t thread_a, thread_b;
    // 子线程A:传递"hello"
    pthread_create(&thread_a, &thread_attr, task, (void *)"hello");
    // 子线程B:传递"world"
    pthread_create(&thread_b, &thread_attr, task, (void *)"world");

    // 主线程睡眠1秒,确保子线程完成任务(否则主线程提前退出会终止子线程)
    sleep(1);
    printf("main thread exit\n");

    // 销毁属性对象(创建线程后即可销毁,不影响已创建的线程)
    pthread_attr_destroy(&thread_attr);
    return 0;
}

示例:线程创建后设置分离属性(pthread_detach)

若创建线程时未设置分离属性,可在线程函数内调用pthread_detach()强制设置:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
void *task(void *arg) {
    // 强制将当前线程设为分离属性(参数为自身ID,通过pthread_self()获取)
    int ret = pthread_detach(pthread_self());
    if (ret != 0) {
        fprintf(stderr, "pthread_detach error[%d]%s\n",errno, strerror(errno));
        pthread_exit(NULL);
    }

    printf("I am thread, my TID: %lu\n", (unsigned long)pthread_self());
    pthread_exit(NULL); // 终止后自动释放资源
}

int main(int argc, char const *argv[]) {
    pthread_t thread;
    // 创建线程(默认可连接状态)
    pthread_create(&thread, NULL, task, NULL);

    // 主线程睡眠1秒,等待子线程执行
    sleep(1);
    printf("main thread exit\n");
    return 0;
}

属性操作函数

image

posted @ 2025-11-17 11:08  YouEmbedded  阅读(4)  评论(0)    收藏  举报