编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

    • Linux应用开发
    • QML基础入门

    • QT核心库实践

    • Linux系统编程

      • Linux系统编程基础
      • 系统调用原理精讲
      • 文件IO深度解析
      • 进程管理深度解析
      • 线程并发深度解读
        • 信号处理深度解读
        • 进程间通信IPC
        • 网络IO多路复用
        • 内存管理深度解读
        • 动态链接与共享库
        • Linux与定时器
      • 综合项目实战

    • IoT智能硬件开发

    • Apps
    • Linux应用开发
    • Linux系统编程
    杨充
    2025-07-02
    目录

    线程并发深度解读

    # 04.线程并发深度解读

    Qt 的 QThread 底层就是 pthread——理解原生线程 API 和同步原语,才能在嵌入式场景下做出正确的并发架构选择,而不是"不管三七二十一全部上 QtConcurrent"。

    # 目录介绍

    • 4.1 案例引入
      • 4.1.1 仪表盘渲染线程莫名其妙卡死
      • 4.1.2 死锁的四个必要条件分析
      • 4.1.3 核心问题
    • 4.2 线程的生命周期
      • 4.2.1 pthread_create 参数全景
      • 4.2.2 线程退出与 pthread_join/detach
      • 4.2.3 线程取消机制 pthread_cancel
      • 4.2.4 嵌入式 CPU 亲和性 (affinity)
    • 4.3 互斥机制全对比
      • 4.3.1 pthread_mutex 类型与 PTHREAD_MUTEX_ERRORCHECK
      • 4.3.2 读写锁 pthread_rwlock 的适用场景
      • 4.3.3 自旋锁 pthread_spinlock 与互斥锁的性能分水岭
      • 4.3.4 嵌入式单核 vs 多核的锁策略差异
    • 4.4 条件变量与同步
      • 4.4.1 pthread_cond_wait 的正确写法(while 循环)
      • 4.4.2 虚假唤醒 (spurious wakeup)
      • 4.4.3 屏障 pthread_barrier
    • 4.5 线程安全与可重入
      • 4.5.1 线程局部存储 (TLS)
      • 4.5.2 errno 的线程安全实现
      • 4.5.3 信号安全的函数列表
    • 4.6 嵌入式线程调优
      • 4.6.1 线程栈大小设置与溢出检测
      • 4.6.2 QoS 与优先级翻转问题
      • 4.6.3 线程池在低内存设备上的实现
    • 4.7 关键结论与速查表

    # 4.1 案例引入

    # 4.1.1 仪表盘渲染线程莫名其妙卡死

    某车载仪表盘使用 Qt 渲染,主线程负责 QML UI,一个 pthread 后台线程负责 CAN 数据读取。测试发现运行 3 小时后渲染线程卡死,但 CAN 线程仍在正常接收数据。

    出问题的代码(简化):

    // CAN 线程
    pthread_mutex_t can_mutex = PTHREAD_MUTEX_INITIALIZER;
    can_data_t latest_can;
    
    void *can_thread(void *arg) {
        while (1) {
            can_frame_t frame = read_can_bus();
            pthread_mutex_lock(&can_mutex);
            parse_can_frame(&latest_can, &frame);
            // ⚠ 在锁内调用了非线程安全的函数
            char *ip = inet_ntoa(latest_can.gateway_addr.sin_addr);
            log_can_ip(ip);
            pthread_mutex_unlock(&can_mutex);
        }
    }
    
    // 渲染线程
    void *render_thread(void *arg) {
        while (1) {
            pthread_mutex_lock(&can_mutex);      // ← 等锁
            display_can_data(&latest_can);
            pthread_mutex_unlock(&can_mutex);
            render_qml_frame();
        }
    }
    

    排查发现:CAN 线程持有互斥锁 can_mutex,在锁内调用了 inet_ntoa——它内部使用静态缓冲区,在 glibc 实现中这个缓冲区由另一个内部锁保护。而渲染线程正等待 can_mutex。问题在 inet_ntoa 的内部锁重入——这不是典型的 ABBA 死锁,而是"锁内调用非线程安全函数"导致的隐蔽死锁。

    inet_ntoa 的线程陷阱:

    // glibc 中 inet_ntoa 的简化实现
    char *inet_ntoa(struct in_addr in) {
        static char buf[16];          // ← 静态缓冲区!所有线程共享
        // 某些实现有内部锁保护此 buf
        snprintf(buf, sizeof(buf), "%d.%d.%d.%d",
                 in.s_addr & 0xFF, (in.s_addr >> 8) & 0xFF,
                 (in.s_addr >> 16) & 0xFF, (in.s_addr >> 24) & 0xFF);
        return buf;
    }
    

    正确做法——用线程安全版本 inet_ntop:

    char ip_str[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &latest_can.gateway_addr.sin_addr, ip_str, sizeof(ip_str));
    log_can_ip(ip_str);               // ip_str 是局部变量,天然线程安全
    

    # 4.1.2 死锁的四个必要条件分析

    死锁的发生必须具备四个条件(同时满足):

    条件 含义 本案例中
    互斥 资源不能共享,同一时间只能一个线程持有 ✅ can_mutex 是互斥锁
    持有并等待 线程持有一个资源的同时等待另一个资源 ✅ CAN 线程持有 can_mutex 等 inet_ntoa 的内部锁
    不可剥夺 资源不能被强制拿走,只能由持有者主动释放 ✅ 互斥锁不能被外部 unlock
    循环等待 多个线程形成等待环路 ✅ 渲染线程等 can_mutex → CAN 线程等内部锁 → 内部锁受另一个未知持有者

    破坏任一条件即可防止死锁。本案例的修复属于破坏条件 2——不在锁内调用任何可能再获取锁的函数。

    预防死锁的四条实战铁律:

    1. 锁内不调外部函数——除非确认该函数不获取任何锁
    2. 固定加锁顺序——所有线程以相同顺序获取多把锁(lock(A); lock(B),不要 lock(B); lock(A))
    3. 用 pthread_mutex_trylock + 回退——获取失败就释放已持有的锁,稍后再试
    4. 用 PTHREAD_MUTEX_ERRORCHECK——检测同一线程重复加锁(提前暴露潜在死锁)

    # 4.1.3 核心问题

    1. 不同锁类型的适用场景——什么时候用 pthread_mutex,什么时候用 spinlock?
    2. pthread_cond_wait 为什么必须写在 while 循环里?
    3. 嵌入式单核 CPU 上的多线程真的是并行吗?锁策略有什么不同?
    4. 什么是"线程安全"和"可重入"?errno 如何在多线程下正确工作?

    # 4.2 线程的生命周期

    # 4.2.1 pthread_create 参数全景

    pthread_create 是 POSIX 线程的入口——底层在 Linux 上调用 clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD, ...):

    #include <pthread.h>
    
    int pthread_create(pthread_t *thread,           // [out] 线程 ID
                       const pthread_attr_t *attr,  // 线程属性(NULL = 默认)
                       void *(*start_routine)(void*), // 线程入口函数
                       void *arg);                  // 传给入口函数的参数
    

    pthread_attr_t 可定制的五项属性:

    属性 设置函数 默认值 说明
    栈大小 pthread_attr_setstacksize 8 MB (glibc) 嵌入式须大幅缩小
    栈地址 pthread_attr_setstackaddr 内核分配 极少使用
    分离状态 pthread_attr_setdetachstate PTHREAD_CREATE_JOINABLE detach 后不可 join
    调度策略 pthread_attr_setschedpolicy SCHED_OTHER 实时线程用 SCHED_FIFO/SCHED_RR
    调度优先级 pthread_attr_setschedparam 0 范围 1-99(实时策略)
    继承调度 pthread_attr_setinheritsched PTHREAD_EXPLICIT_SCHED 是否继承父线程调度

    完整创建示例:

    pthread_t tid;
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    
    // 嵌入式定制:缩小栈到 128 KB
    pthread_attr_setstacksize(&attr, 128 * 1024);
    
    // 分离线程:不需要 pthread_join
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    
    if (pthread_create(&tid, &attr, can_thread_func, NULL) != 0) {
        perror("pthread_create");
    }
    pthread_attr_destroy(&attr);
    

    Qt 侧对应:

    // QThread(底层用 pthread_create)
    class CanThread : public QThread {
        void run() override { /* can_thread_func 的逻辑 */ }
    };
    CanThread *t = new CanThread;
    t->setStackSize(128 * 1024);  // Qt 封装的栈大小设置
    t->start();
    
    // QtConcurrent(底层用 QThreadPool + pthread)
    QtConcurrent::run(&readCANData);
    

    # 4.2.2 线程退出与 pthread_join/detach

    线程退出的四种方式:

    方式 说明 后果
    return (从线程函数返回) 最干净 返回值可通过 pthread_join 获取
    pthread_exit(retval) 显式退出当前线程 返回值传 retval
    pthread_cancel(tid) 被其他线程取消 返回 PTHREAD_CANCELED
    进程终止 (exit / 信号) 整个进程退出 所有线程一并消失

    pthread_join——等一个线程结束并收尸:

    void *result;
    int rc = pthread_join(tid, &result);
    if (rc == 0) {
        printf("thread returned: %p\n", result);
    }
    

    pthread_join 相当于进程的 waitpid——不 join 的线程会变成"线程僵尸"(占用少量内核资源,类似进程僵尸)。

    pthread_detach——告诉内核"我不会 join 它,它退出后自动回收":

    // 方式 1:创建时指定
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    pthread_create(&tid, &attr, func, NULL);
    
    // 方式 2:创建后 detach
    pthread_detach(tid);
    

    join vs detach 选择:

    场景 推荐
    需要获取线程返回值 join
    线程退出需要同步点 join
    一次性任务(fire-and-forget) detach
    长期运行的后台线程 detach
    Qt 的 QThread Qt 框架内部已处理,不需手动 join/detach

    # 4.2.3 线程取消机制 pthread_cancel

    pthread_cancel 向目标线程发送"取消请求",线程在下一个取消点(cancellation point)检查该请求并退出。

    // 请求取消
    pthread_cancel(tid);
    pthread_join(tid, NULL);   // 等待真正退出
    

    取消状态和类型:

    // 设置是否接受取消
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);   // 默认
    pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);  // 推迟取消(临界区保护)
    
    // 设置取消时机
    pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);  // 默认:推迟到取消点
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); // 立即取消(危险!)
    

    异步取消的危险:

    // ⚠ 灾难性代码
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
    pthread_mutex_lock(&mutex);     // 可能在这里被取消!
    // ... 受保护的资源 ...
    pthread_mutex_unlock(&mutex);   // ← 永远执行不到,锁泄漏!
    

    POSIX 定义的取消点(部分常用函数):

    类别 函数
    I/O open, read, write, close, fsync, select, poll, sleep
    同步 pthread_mutex_lock, pthread_cond_wait, pthread_join
    标准库 printf, fprintf, fread, fwrite, getchar, putchar

    安全取消的最佳实践——清理处理器:

    void cleanup_mutex(void *arg) {
        pthread_mutex_unlock((pthread_mutex_t *)arg);
    }
    
    void *safe_thread(void *arg) {
        pthread_mutex_lock(&mutex);
        // 注册清理函数——如果在这里被取消,cleanup 会自动 unlock
        pthread_cleanup_push(cleanup_mutex, &mutex);
    
        // ... 危险操作,可能在取消点被取消 ...
    
        pthread_cleanup_pop(1);    // 参数 1 = 执行 cleanup 函数
        // pthread_mutex_unlock(&mutex);  // cleanup 已执行,不需要手动 unlock
        return NULL;
    }
    

    嵌入式最佳实践:尽量不用 pthread_cancel。用标志位 + 条件变量通知线程自行退出——可控且安全。

    volatile int quit = 0;
    
    void *worker(void *arg) {
        while (!quit) {
            do_work();
        }
        cleanup_resources();
        return NULL;
    }
    
    // 主线程通知退出:
    quit = 1;
    pthread_join(tid, NULL);   // 等线程安全退出
    

    # 4.2.4 嵌入式 CPU 亲和性 (affinity)

    CPU 亲和性(affinity)——把线程绑定到指定的 CPU 核心上,减少内核调度带来的缓存失效。

    #define _GNU_SOURCE
    #include <sched.h>
    #include <pthread.h>
    
    // 绑定到 CPU 0
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(0, &cpuset);
    pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);
    
    // 绑定到 CPU 0 或 CPU 1(调度器二选一)
    CPU_ZERO(&cpuset);
    CPU_SET(0, &cpuset);
    CPU_SET(1, &cpuset);
    pthread_setaffinity_np(tid, sizeof(cpuset), &cpuset);
    

    嵌入式 affinity 的典型策略:

    4 核 ARM (CPU 0-3)
    
    CPU 0: 实时 CAN 线程     (SCHED_FIFO, prio 90)  ← 必须响应 < 1ms
    CPU 1: 渲染线程           (SCHED_OTHER)          ← 需要稳定的 GPU 调度
    CPU 2: I/O + 日志线程     (SCHED_OTHER)          ← 不与 CAN 抢 CPU 0
    CPU 3: 通用线程池         (SCHED_OTHER)          ← 剩下的杂活
    

    affinity 的好处与陷阱:

    好处 说明
    避免缓存失效 线程不跨核心迁移,L1/L2 缓存热点保持
    关键线程隔离 CAN/实时线程独占核心,不被中断/其他线程干扰
    减少调度延迟 SCHED_FIFO + 独占核心 = 确定性时延
    陷阱 说明
    过热风险 独占核心一直工作,芯片过热→降频→性能更差
    误配负载 把太多线程绑到一个核心,其他核心闲置
    tickless 冲突 独占核心不触发 tick,但 sleep/nanosleep 依赖 tick

    # 4.3 互斥机制全对比

    # 4.3.1 pthread_mutex 类型与 PTHREAD_MUTEX_ERRORCHECK

    四种互斥锁类型:

    类型 重复 lock 跨线程 unlock 行为
    PTHREAD_MUTEX_NORMAL 死锁 未定义 最基础,无检查(性能最优)
    PTHREAD_MUTEX_ERRORCHECK 返回 EDEADLK 返回 EPERM 开发/调试阶段推荐
    PTHREAD_MUTEX_RECURSIVE 允许(计数) 返回 EPERM 递归锁(需要同一函数多次锁)
    PTHREAD_MUTEX_DEFAULT 未定义 未定义 行为取决于实现(= NORMAL)

    错误检查锁的用法(开发阶段兜底):

    pthread_mutex_t mutex;
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);
    pthread_mutex_init(&mutex, &attr);
    
    // 同一线程重复 lock:
    pthread_mutex_lock(&mutex);     // 第 1 次 → 成功
    int rc = pthread_mutex_lock(&mutex);  // 第 2 次 → 返回 EDEADLK,不死锁!
    if (rc == EDEADLK) {
        fprintf(stderr, "BUG: recursive lock detected!\n");
        // 立即发现潜在的递归加锁 bug
    }
    

    为什么 ERRORCHECK 是开发期神器?

    在你加 assert(rc != EDEADLK) 的那一刻,第一次重复加锁就会在测试阶段暴露——而不是上线后变成"不崩但卡死"的幽灵 bug。

    递归锁的典型场景——模板方法模式:

    // 公共接口(需要加锁)
    void update_config(const char *key, const char *val) {
        pthread_mutex_lock(&mutex);      // 第 1 次 lock
        _update(key, val);
        pthread_mutex_unlock(&mutex);
    }
    
    // 内部实现(也加锁——保证直接调用时线程安全)
    void reload_all_config() {
        pthread_mutex_lock(&mutex);      // 第 2 次 lock(同一线程,递归)
        for (all keys) _update(key, read_from_disk(key));
        pthread_mutex_unlock(&mutex);
    }
    

    这里如果不用 RECURSIVE 锁,reload_all_config 里调 _update 之前已经持锁,再调 update_config(内部也试图 lock)就会死锁。

    # 4.3.2 读写锁 pthread_rwlock 的适用场景

    读写锁允许多个读者同时读,但写者独占:

    pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
    
    // 多个线程可以同时持有读锁
    void read_data() {
        pthread_rwlock_rdlock(&rwlock);
        display(latest_can);
        pthread_rwlock_unlock(&rwlock);
    }
    
    // 写者需要等所有读者释放
    void write_data() {
        pthread_rwlock_wrlock(&rwlock);
        update_can_data(&latest_can);
        pthread_rwlock_unlock(&rwlock);
    }
    

    读写锁的适用场景判据:

    条件 用 rwlock 用 mutex
    读远多于写(> 10:1) ✅ ❌(mutex 阻止并发读)
    临界区极短(< 1μs) ❌ ✅(rwlock 内部开销更大)
    读/写比例接近 ❌ ✅(rwlock 无收益 + 写者饥饿风险)
    必须高优先级写者优先 ❌ ✅(默认 rwlock 可能读者饿写者)

    rwlock 的性能陷阱——rwlock 内部维护读者计数和等待队列,本身比 mutex 重。如果临界区只有几个纳秒,rwlock 的内部开销反而更大。

    实测数据(ARM Cortex-A7,临界区 = 10 ns):

    mutex lock/unlock:   ~50ns
    rwlock rdlock/unlock: ~80ns   ← 反而慢
    rwlock wrlock/unlock: ~90ns
    

    结论:rwlock 适用于读占 90%+ 且临界区 > 1 μs的场景。否则用 mutex。

    # 4.3.3 自旋锁 pthread_spinlock 与互斥锁的性能分水岭

    自旋锁(spinlock)——拿不到锁时不睡眠,而是在 CPU 上忙等(一直while 检查锁状态)。

    pthread_spinlock_t spin;
    pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);
    
    pthread_spin_lock(&spin);
    // ← 如果锁被持有,当前 CPU 进入 busy-wait 循环
    //   不会触发上下文切换
    critical_section();
    pthread_spin_unlock(&spin);
    

    spinlock vs mutex 的选择分水岭:

    条件 用 spinlock 用 mutex
    临界区极短(< 1 μs) ✅ ❌(上下文切换开销 > 忙等开销)
    临界区有 I/O 或睡眠 ❌ ✅(忙等期间一直占 CPU)
    持锁时间有保证 ✅ ❌
    单核 CPU ❌ ✅(忙等期间其他线程无法运行)
    不确定持锁时间 ❌ ✅
    必须在中断上下文中用锁 ✅(只有 spinlock 可用) ❌(中断中不能睡眠)

    性能关键数据(x86_64 典型值):

    操作 耗时
    一次上下文切换 ~1-3 μs
    mutex lock (无竞争) ~25 ns
    mutex lock (有竞争) ~1-3 μs(一次 futex 系统调用)
    spinlock lock (无竞争) ~10 ns
    spinlock lock (有竞争,等 100ns) ~110 ns
    spinlock lock (有竞争,等 1μs) ~1 μs

    决策规则:临界区 < 上下文切换开销 → spinlock;临界区 > 上下文切换开销 → mutex。

    // ✅ spinlock 适用:仅仅是一个原子更新
    pthread_spin_lock(&spin);
    counter++;              // 1 条指令
    pthread_spin_unlock(&spin);
    
    // ✅ mutex 适用:涉及 I/O
    pthread_mutex_lock(&mutex);
    write(fd, buf, len);    // 可能阻塞几 ms
    pthread_mutex_unlock(&mutex);
    

    # 4.3.4 嵌入式单核 vs 多核的锁策略差异

    单核 CPU 上的锁几乎全是 mutex——因为在单核上 spinlock 意味着"唯一的核心在忙等,其他线程无法运行",直到内核时间片耗尽。spinlock 在单核上几乎没价值。

    ARM Cortex-A 内核的 LDREX/STREX 指令——这是所有锁的硬件基础:

    ; ARM 上的原子 compare-and-swap (LDREX/STREX 对)
    ldrex r1, [r0]          ; 独占加载
    cmp r1, #0              ; 比较旧值
    bne fail
    mov r2, #1
    strex r3, r2, [r0]      ; 独占存储——如果失败,r3=1
    cmp r3, #0
    bne retry                ; 失败 → 重试
    

    x86 LOCK CMPXCHG 的差异:

    ; x86 上更简单——LOCK 前缀保证原子性
    lock cmpxchg [rdi], rsi  ; 原子 compare-and-swap
    ; LOCK 指令自动处理缓存一致性和内存屏障
    

    单核 vs 多核锁策略对比:

    方面 单核 多核
    spinlock 价值 几乎无(竞品不可运行) 有(其他核心可并行)
    mutex 实现 只需禁用抢占 需要 futex + 内核调度
    伪共享(false sharing) 无 严重时性能下降 10x
    缓存一致性协议 无 MESI 协议会自动在核心间广播

    多核下的伪共享(false sharing)问题:

    // ❌ 两个相邻变量被不同核上的线程频繁修改
    struct {
        volatile int counter_a;   // CPU 0 的线程写
        volatile int counter_b;   // CPU 1 的线程写
    } counts;                     // 两个变量在同一缓存行内(64 字节)
    
    // 每次 counter_a 写 → CPU 1 的缓存行失效 → counter_b 读要重新加载
    // 性能可能下降 10-50 倍
    
    // ✅ 修复:用 __attribute__((aligned(64))) 或 padding
    struct {
        volatile int counter_a;
        char __pad[60];           // 填充到 64 字节
        volatile int counter_b;
        char __pad2[60];
    } counts;
    

    # 4.4 条件变量与同步

    # 4.4.1 pthread_cond_wait 的正确写法(while 循环)

    条件变量解决"生产者-消费者"问题——消费者等数据就绪,生产者通知消费者。

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    can_frame_t buffer;
    int buffer_ready = 0;
    
    // 生产者
    void produce(can_frame_t frame) {
        pthread_mutex_lock(&mutex);
        buffer = frame;
        buffer_ready = 1;
        pthread_cond_signal(&cond);       // 唤醒一个消费者
        pthread_mutex_unlock(&mutex);
    }
    
    // 消费者 —— 正确写法
    void *consumer(void *arg) {
        pthread_mutex_lock(&mutex);
        while (!buffer_ready) {          // ← while,不是 if!
            pthread_cond_wait(&cond, &mutex); // 原子操作:unlock mutex + 睡眠
                                              // 被唤醒后自动 re-lock mutex
        }
        process_frame(buffer);
        buffer_ready = 0;
        pthread_mutex_unlock(&mutex);
    }
    

    为什么必须是 while 而不是 if?

    两个原因:

    1. 虚假唤醒(见 4.4.2)
    2. 多消费者竞争——signal 只唤醒一个线程,但被唤醒后发现 buffer_ready 已被另一个消费者清 0
    // ❌ 错误写法
    if (!buffer_ready) {
        pthread_cond_wait(&cond, &mutex);
    }
    // ← 被唤醒后 buffer_ready 可能还是 0!
    // 因为另一个线程抢先消费了
    
    // ✅ 正确写法
    while (!buffer_ready) {
        pthread_cond_wait(&cond, &mutex);
    }
    // ← 被唤醒后重新检查条件,不满足继续等
    

    pthread_cond_wait 的三步原子语义:

    pthread_cond_wait(&cond, &mutex)
      ├─ 1. 原子地:unlock mutex + 把当前线程放入 cond 等待队列
      ├─ 2. 线程睡眠,等待被 signal 唤醒
      └─ 3. 被唤醒时:自动 re-lock mutex,然后返回
    

    这保证在"unlock → 睡眠"之间没有窗口让 signal 丢失。

    # 4.4.2 虚假唤醒 (spurious wakeup)

    虚假唤醒——pthread_cond_wait 返回,但没有线程调用过 signal 或 broadcast。这在 POSIX 标准中是合法的行为,原因是:

    1. 多核架构:内核为了简化实现,可能被中断/信号干扰提前返回
    2. 性能优化:某些 OS 实现中,broadcast 可能导致多个线程同时被唤醒,其中只有一个能真正消费数据
    // ❌ 如果这样写,虚假唤醒会导致用无效数据处理
    if (!buffer_ready)
        pthread_cond_wait(&cond, &mutex);
    // 假设虚假唤醒发生——跳过了 wait
    // buffer_ready 还是 0 —— 但代码以为数据已就绪!
    process_frame(buffer);  // ← 处理未初始化的数据!
    
    // ✅ while 循环同时防御虚假唤醒和竞争唤醒
    while (!buffer_ready)
        pthread_cond_wait(&cond, &mutex);
    

    pthread_cond_broadcast——唤醒所有等待线程(而非只唤醒一个):

    // 场景:资源池扩容,所有等待线程都可以重试
    resource_count += 10;
    pthread_cond_broadcast(&cond);    // 唤醒所有人
    

    broadcast 被唤起的线程会同时竞争 re-lock mutex——只有一个拿锁,其余继续在 mutex 上等。

    # 4.4.3 屏障 pthread_barrier

    屏障(barrier)——让多个线程在同一个点同步。所有线程都到达屏障后,才一起继续执行。

    pthread_barrier_t barrier;
    pthread_barrier_init(&barrier, NULL, 3);   // 3 个线程参与
    
    void *worker(void *arg) {
        int phase = *(int *)arg;
        printf("Phase %d: working\n", phase);
        do_phase_work(phase);
    
        // 所有线程在这里等——等 3 个线程都到达才一起继续
        pthread_barrier_wait(&barrier);
    
        printf("Phase %d: all synced, proceeding\n", phase);
        return NULL;
    }
    

    屏障的典型应用——并行矩阵乘法:

    #define N 512
    double A[N][N], B[N][N], C[N][N];
    
    void *matmul_worker(void *arg) {
        int tid = *(int *)arg;
        int rows_per_thread = N / NUM_THREADS;
        int start = tid * rows_per_thread;
        int end = start + rows_per_thread;
    
        // 阶段 1:每个线程计算自己负责的行
        for (int i = start; i < end; i++)
            for (int j = 0; j < N; j++)
                for (int k = 0; k < N; k++)
                    C[i][j] += A[i][k] * B[k][j];
    
        pthread_barrier_wait(&barrier);   // 等所有线程完成阶段 1
    
        // 阶段 2:汇总结果 —— 此时 C 全部计算完毕
        // ...
    }
    

    嵌入式场景:屏障常用于多传感器同步采集——所有传感器线程同时开始采样,确保数据时间戳一致。


    # 4.5 线程安全与可重入

    # 4.5.1 线程局部存储 (TLS)

    TLS(Thread-Local Storage)——每个线程拥有变量的独立副本,互不干扰。

    方式 1:__thread 关键字(GCC/Clang)

    // 声明为 TLS——每个线程一个独立副本
    __thread int errno_stub = 0;
    __thread char log_buffer[256];
    
    void *worker(void *arg) {
        snprintf(log_buffer, sizeof(log_buffer), "thread %lu", pthread_self());
        // 不同线程的 log_buffer 互不影响
    }
    

    方式 2:pthread_key_t(动态 TLS)

    pthread_key_t key;
    
    // 创建 key,可选的析构函数在线程退出时清理
    pthread_key_create(&key, free);   // free 是析构函数
    
    // 每个线程设置自己的值
    void *worker(void *arg) {
        char *my_data = malloc(64);
        pthread_setspecific(key, my_data);
        // ...
        char *data = pthread_getspecific(key);
        printf("[%lu] %s\n", pthread_self(), data);
    }
    

    TLS 的两大局限:

    局限 说明
    __thread 只能用于 POD 类型 不能用于 C++ 带构造函数的对象(C++ 用 thread_local)
    动态 TLS 有数量限制 PTHREAD_KEYS_MAX,通常 128-1024

    # 4.5.2 errno 的线程安全实现

    errno 是 TLS 的经典应用——每个线程有自己的 errno 副本:

    // 宏展开后的等价代码(简化):
    extern __thread int __errno_location;
    #define errno (*&__errno_location)
    

    为什么 errno 必须是 TLS?

    // 如果没有 TLS,多线程下 errno 会被覆盖:
    Thread A: open("/foo", O_RDONLY) → 失败 → errno = ENOENT
    Thread B: write(fd1, buf, 100) → 成功 → errno = 0
    Thread A: perror("open") → errno 已经是 0 → 打印 "Success" ← 错!
    

    正确取 errno 的姿势——立即保存:

    int fd = open("/foo", O_RDONLY);
    int saved_errno = errno;     // ← 立刻保存!下一行可能覆盖
    if (fd < 0) {
        fprintf(stderr, "open failed: %s\n", strerror(saved_errno));
    }
    

    # 4.5.3 信号安全的函数列表

    信号处理器中能调的函数极为有限——POSIX 定义了"async-signal-safe"列表。在信号处理器中调用非安全的函数 = 未定义行为。

    async-signal-safe 函数速查:

    类别 函数
    基础 I/O _exit, write, read, close, open, fsync
    信号 signal, sigaction, kill, raise, sigprocmask
    时间 time, clock_gettime
    Unix getpid, getuid, getppid, unlink, rename, mkdir
    socket socket, bind, connect, accept, send, recv

    不安全的函数(不能在信号处理器中用):

    printf, malloc, free, fopen, fclose, pthread_mutex_lock,
    system, popen, exec, exit (用 _exit 代替)
    

    ARM 上信号处理器的额外限制——某些 ARM 核在信号处理器中用浮点运算导致寄存器损坏(因为 FPU 寄存器不在信号栈上自动保存)。用 SA_ONSTACK + 汇编在信号处理器入口保存/恢复 FPU 状态。


    # 4.6 嵌入式线程调优

    # 4.6.1 线程栈大小设置与溢出检测

    glibc 默认线程栈 = 8 MB——在嵌入式 128 MB 内存设备上,10 个线程就吃掉 80 MB 虚拟地址空间。必须缩小:

    pthread_attr_t attr;
    pthread_attr_init(&attr);
    
    // 嵌入式典型设置(根据实际栈用量调整)
    size_t stack_size = 128 * 1024;    // 128 KB
    pthread_attr_setstacksize(&attr, stack_size);
    
    pthread_create(&tid, &attr, thread_func, NULL);
    

    确定栈大小的经验公式:

    栈大小 >= 最深调用链栈帧总和 + 安全余量 (至少 2 KB)
    
    测量方法:
    1. 运行时用 pthread_attr_getstack 或 /proc/<tid>/maps 查实际使用
    2. 用编译器 -fstack-usage 得到每个函数的栈帧大小
    3. 用 valgrind --tool=massif --stacks=yes 跟踪实际栈增长
    

    栈溢出检测方案:

    // 方式 1:守护页(guard page)——默认已启用
    // 栈底放置 1 页不可访问内存,溢出时 → SIGSEGV
    pthread_attr_setguardsize(&attr, 4096);  // 默认 4 KB
    
    // 方式 2:编译器栈检查(Development build)
    // GCC: -fstack-protector-strong
    // 在函数入口放入 canary,返回时检查
    // 溢出 → __stack_chk_fail → abort
    
    // 方式 3:运行时检测
    void check_stack_usage() {
        pthread_attr_t attr;
        void *stack_addr;
        size_t stack_size;
        pthread_getattr_np(pthread_self(), &attr);
        pthread_attr_getstack(&attr, &stack_addr, &stack_size);
    
        // 读取当前 SP(粗略估计栈使用深度)
        void *sp;
        asm volatile("mov %0, sp" : "=r"(sp));
        size_t used = (char *)stack_addr + stack_size - (char *)sp;
        printf("stack used: %zu / %zu KB\n", used / 1024, stack_size / 1024);
    }
    

    # 4.6.2 QoS 与优先级翻转问题

    优先级翻转(Priority Inversion)——高优先级线程因等低优先级线程持有的锁而阻塞,而中等优先级线程可能在两者之间抢占 CPU,导致高优先级线程无限期等待。

    经典场景:

    Thread H (prio 90, 实时):  要拿 mutex → 阻塞(等 L 释放)
    Thread M (prio 50, 普通):  计算密集 → 运行中
    Thread L (prio 10, 最低):  持有 mutex → 被 M 抢占 → L 得不到 CPU → 不放锁
                               → H 永远被 M 阻塞 ← 优先级翻转!
    

    解决方案——优先级继承(PI, Priority Inheritance):

    // 创建支持优先级继承的互斥锁
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);  // ← 关键
    pthread_mutex_init(&mutex, &attr);
    

    PI 机制下:当 H 阻塞等 L 的锁时,L 的优先级自动提升到 90(H 的优先级)→ M 无法抢占 L → L 快速完成释放锁 → H 拿到锁。

    Mars Pathfinder 的真实教训(1997 年 NASA 火星探测器):

    信息总线线程 (prio 高): 等气象数据互斥锁 → 阻塞
    通信线程 (prio 中): 长期运行 → 一直占 CPU
    气象线程 (prio 低): 持锁 → 被通信线程抢占 → 一直不放锁
    → 信息总线线程被看门狗认为是"死掉" → 系统重置
    → 修复:开启优先级继承(从地球上打补丁到火星)
    

    # 4.6.3 线程池在低内存设备上的实现

    线程池避免频繁创建/销毁线程的开销。嵌入式版本——固定线程数 + 任务队列 + 条件变量:

    #define MAX_THREADS 4
    #define MAX_TASKS   128
    
    typedef struct {
        void (*func)(void *);
        void *arg;
    } task_t;
    
    typedef struct {
        pthread_mutex_t lock;
        pthread_cond_t work_cond;
        pthread_cond_t done_cond;
        pthread_t threads[MAX_THREADS];
        task_t tasks[MAX_TASKS];
        int head, tail, count;
        int active_count;
        int quit;
    } thread_pool_t;
    
    // 工作线程
    void *pool_worker(void *arg) {
        thread_pool_t *pool = (thread_pool_t *)arg;
        while (1) {
            pthread_mutex_lock(&pool->lock);
            while (pool->count == 0 && !pool->quit) {
                pthread_cond_wait(&pool->work_cond, &pool->lock);
            }
            if (pool->quit && pool->count == 0) {
                pthread_mutex_unlock(&pool->lock);
                break;
            }
            task_t task = pool->tasks[pool->head];
            pool->head = (pool->head + 1) % MAX_TASKS;
            pool->count--;
            pool->active_count++;
            pthread_mutex_unlock(&pool->lock);
    
            // 执行任务
            task.func(task.arg);
    
            pthread_mutex_lock(&pool->lock);
            pool->active_count--;
            if (pool->count == 0 && pool->active_count == 0) {
                pthread_cond_signal(&pool->done_cond);  // 通知 wait_all
            }
            pthread_mutex_unlock(&pool->lock);
        }
        return NULL;
    }
    
    // 提交任务
    void pool_submit(thread_pool_t *pool, void (*func)(void*), void *arg) {
        pthread_mutex_lock(&pool->lock);
        int tail = (pool->head + pool->count) % MAX_TASKS;
        pool->tasks[tail].func = func;
        pool->tasks[tail].arg = arg;
        pool->count++;
        pthread_cond_signal(&pool->work_cond);   // 唤醒一个工作线程
        pthread_mutex_unlock(&pool->lock);
    }
    

    嵌入式线程池的关键参数:

    参数 推荐值 原因
    线程数 CPU 核数 超过核数只会增加上下文切换,无收益
    队列大小 128-256 大了浪费内存,小了的任务会被阻塞
    线程栈 64-128 KB 减小内存占用
    空闲回收 不回收 嵌入式通常以设备生命周期为单位,无重启

    Qt 侧直接用 QThreadPool ——它内建了全局线程池,且已在 ARM 上充分优化:

    QThreadPool::globalInstance()->setMaxThreadCount(4);
    QtConcurrent::run(&processCANData);
    

    # 4.7 关键结论与速查表

    核心结论五条:

    1. 线程共享地址空间,进程隔离地址空间——这是最根本的区别。线程的并发优势(低开销、共享数据)和并发劣势(数据竞争、死锁)都源于此
    2. 锁的选择 = 临界区长度 + 竞争强度:临界区 < 1 μs → spinlock;临界区 > 10 μs → mutex;读占 90%+ 且临界区长 → rwlock
    3. 条件变量必须 while——虚假唤醒和竞争唤醒都会让条件在 wait 返回时不再成立
    4. 嵌入式单核上 spinlock 几乎无意义——忙等期间唯一的核心被占用,其他线程无法取得进展
    5. pthread_cancel 是异步炸弹——用标志位 + 条件变量让线程主动退出,永远更安全

    三种锁速查表:

    锁类型 持锁期间是否睡眠 竞争时行为 开销 适用
    pthread_mutex 可以 futex 阻塞 中等 通用,临界区 > 1 μs
    pthread_spinlock 绝对不能 CPU 忙等 极低 临界区 < 1 μs,多核
    pthread_rwlock 读锁不影响其他读 写者阻塞 较高 读多写少
    pthread_mutex(RECURSIVE) 可以 futex 阻塞 中等 同一线程需要重入
    pthread_mutex(ERRORCHECK) 可以 死锁 → 返回 EDEADLK 中等 开发/调试阶段

    线程属性速查表:

    属性 推荐嵌入式默认 说明
    栈大小 64-128 KB 够绝大多数传感器/日志线程
    detach 状态 PTHREAD_CREATE_DETACHED 无需 join 时用
    CPU 亲和性 CAN/实时线程绑核,其他自由 避免热缓存失效
    调度策略 SCHED_OTHER(普通) 实时需 CAP_SYS_NICE 权限
    优先级继承 PTHREAD_PRIO_INHERIT 总是开启

    常见陷阱速查:

    陷阱 根因 解决
    死锁 多锁获取顺序不一致 固定加锁顺序 + ERRORCHECK
    优先级翻转 低优先线程持锁被中优先抢占 开启 PI mutex
    伪共享 多核访问同一缓存行的不同变量 aligned(64) 或 padding
    锁内调非线程安全函数 函数使用全局静态缓冲区 改用线程安全版本(_r 后缀)
    pthread_cond_wait 用 if 虚假唤醒或竞争唤醒 改用 while
    栈溢出 嵌入式默认 8 MB 太浪费 缩小到 64-128 KB + guard page

    下一步:在你的 Qt 仪表盘项目里用 pthread_mutexattr_setprotocol(PTHREAD_PRIO_INHERIT) 替换现有的普通 mutex——然后跑一轮压力测试,看 CAN 线程的调度延迟是否从"偶发的几十 ms"降到"稳定 < 1 ms"。


    延伸阅读:

    • man 7 pthreads —— POSIX 线程概览
    • man 3 pthread_mutexattr_setprotocol —— 优先级继承
    • man 2 futex —— mutex 的底层内核机制
    • 《Programming with POSIX Threads》(David Butenhof) —— 线程编程圣经
    • 《The Linux Programming Interface》(Kerrisk) —— 第 29-33 章
    上次更新: 2026/07/12, 19:39:51
    进程管理深度解析
    信号处理深度解读

    ← 进程管理深度解析 信号处理深度解读→

    最近更新
    01
    audit
    07-27
    02
    C++入门教程全章思考题汇编
    07-24
    03
    12.技术团队建设能力
    07-21
    更多文章>
    Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式