嵌入式Linux多线程编程


1. 什么是线程?

1.1 线程的本质
线程是操作系统调度的最小执行单元,共享进程的资源(内存、文件等),但拥有独立的:

  • 栈空间(存储局部变量)
  • 寄存器状态(程序计数器等)
  • 线程ID和优先级

1.2 线程 vs 进程对比

进程
代码段
数据段
文件描述符
信号处理器
线程1
线程2
寄存器
寄存器

1.3 Linux线程实现原理
Linux通过轻量级进程(LWP)实现线程,关键系统调用:

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
  • CLONE_VM:共享内存空间
  • CLONE_FS:共享文件系统信息
  • CLONE_FILES:共享文件描述符表

2. 为什么需要多线程?
场景类型 优势体现 嵌入式案例
响应优化 分离阻塞操作 串口通信+UI响应分离
性能提升 多核CPU并行计算 图像处理流水线
资源复用 共享内存减少拷贝开销 多传感器数据融合
模块解耦 功能隔离降低复杂度 网络协议栈分层实现

3. 多线程基础编程

3.1 POSIX线程库(C语言)

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

void *thread_function(void *arg) {
    printf("Thread start\n");
    sleep(1);
    printf("Thread end\n");
    pthread_exit(NULL);
}

int main(int argc, char *argv[]) {
    pthread_t thread;
    pthread_create(&thread, NULL, thread_function, NULL);
    pthread_join(thread, NULL);//创建的线程一定要join,否则主线程会先结束,子线程会变成僵尸线程,还会存在内存泄漏
    //如果希望线程结束后自动释放资源,可以调用pthread_detach,但是detach的线程不能被join
    //pthread_detach(thread);
    //sleep(1);
    printf("Thread joined\n");
    return 0;
}

3.2 C++11线程库

#include <thread>
#include <iostream>

void thread_function() {
    std::cout << "Thread started" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Thread finished" << std::endl;
}

int main(int argc, char *argv[]) {
    std::thread t1(thread_function);
    t1.join();//创建的线程一定要join,否则主线程会先结束,子线程会变成僵尸线程,还会存在内存泄漏
    //detach 分离线程,子线程结束后会自动释放资源,不会变成僵尸线程,但是detach的线程不能被join
    //t1.detach();
    //sleep(1);
    std::cout << "Main thread finished" << std::endl;
    return 0;
}

4. 多线程进阶:线程池的诞生

4.1 为什么需要线程池?

频繁创建线程
高系统开销
内存碎片
响应延迟
系统不稳定
线程池解决方案

4.2 线程池核心原理
线程池就是提前创建好一堆线程,所有线程阻塞等待一个队列,当用户提交一个任务,就唤醒一个线程执行任务。往复循环。

主线程
提交任务
任务队列
工作线程
线程1
线程2
线程3
...
执行任务
执行任务
执行任务
返回结果
主线程
  1. 任务提交阶段

    • 主线程创建任务对象(函数/可调用对象)
    • 任务被放入线程安全的任务队列(典型实现:环形缓冲区+互斥锁)
  2. 任务分配阶段

    • 工作者线程(Worker Thread)通过条件变量等待任务
    std::unique_lock<std::mutex> lock(queue_mutex);
    condition.wait(lock, [this]{ return !tasks.empty() || stop; });
    
    • 当队列非空时,线程被唤醒并获取任务
  3. 任务执行阶段

    • 线程从队列取出任务(FIFO或优先级策略)
    • 执行用户定义的业务逻辑
    • 可设置超时机制防止线程阻塞
  4. 结果处理阶段

    • 通过回调函数返回结果
    • 或使用std::future获取异步结果
    auto future = pool.enqueue([] { return "Result"; });
    std::cout << future.get(); // 阻塞获取结果
    
  5. 线程管理机制

    创建
    无任务
    有新任务
    异常
    池关闭
    线程状态机
    运行
    等待
    销毁
    终止
    • 动态扩缩容:根据负载自动增减线程数
    • 保活机制:空闲线程超时自动回收
    • 异常处理:线程崩溃后自动重启

5. 线程池实现与使用

5.1 嵌入式友好型C++11线程池
C++标准库没有线程池,线程池本身非常简单,几十行代码的事情,难的是通用且高效的异步调度模型,简单写一个C++线程池demo。

#include <thread>
#include <iostream>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <functional>
#include <vector>
#include <chrono>

// 线程池ThreadPool:用于管理一组工作线程,统一调度和执行任务,避免频繁创建/销毁线程带来的开销
class ThreadPool {
public:
    // 构造函数,初始化线程池并启动指定数量的工作线程
    ThreadPool(size_t threads) : stop(false) {
        for(size_t i=0; i<threads; ++i) {
            // 每个工作线程循环等待任务队列中的任务
            workers.emplace_back([this] {
                while(1) {
                    std::function<void()> task;
                    {
                        std::unique_lock<std::mutex> lock(this->queue_mutex);
                        // 条件变量等待:任务到来或线程池停止
                        this->condition.wait(lock, [this]{ 
                            return this->stop || !this->tasks.empty(); 
                        });
                        // 如果线程池停止且任务队列为空,则退出线程
                        if(stop && tasks.empty()) return;
                        // 取出一个任务
                        task = std::move(this->tasks.front());
                        this->tasks.pop();
                    }
                    // 执行任务
                    task(); 
                }
            });
        }
    }
    
    // 向线程池提交任务,任务类型为可调用对象(如lambda、函数等)
    // 返回值:true表示成功入队,false表示队列已满
    template<class F>
    bool enqueue(F&& f) {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            if(tasks.size() >= max_queue) return false; // 队列满时拒绝新任务
            tasks.emplace(std::forward<F>(f)); // 入队
        }
        condition.notify_one(); // 通知一个工作线程
        return true;
    }
    
    // 析构函数:应负责安全关闭线程池并释放资源(此处略)
    ~ThreadPool() { /* 清理代码略 */ }
    
private:
    std::vector<std::thread> workers; // 工作线程容器
    std::queue<std::function<void()>> tasks; // 任务队列,存储待执行任务
    std::mutex queue_mutex; // 保护任务队列的互斥锁
    std::condition_variable condition; // 条件变量,用于线程间同步
    bool stop; // 线程池停止标志
    const size_t max_queue = 1000; // 任务队列最大长度,防止溢出
};

void thread_function(int i) {
    std::cout << "Thread " << i << " started" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Thread " << i << " finished" << std::endl;
}

int main(int argc, char *argv[]) {
    ThreadPool pool(4);
    for(int i=0; i<5; ++i) {
        pool.enqueue(std::bind(thread_function, i));
    }
    std::this_thread::sleep_for(std::chrono::seconds(10));
    return 0;
}


6. 调试与监控工具链

6.1 GDB多线程调试

$ gdb -p <PID>
(gdb) info threads    # 查看线程列表
(gdb) thread 2        # 切换到线程2
(gdb) bt full         # 查看完整调用栈
(gdb) p var@main      # 查看主线程变量

6.2 性能分析工具

  • perf:CPU使用分析
    perf record -g -p <PID>   # 采样
    perf report               # 生成报告
    
  • 火焰图:可视化性能瓶颈
    perf script | FlameGraph/stackcollapse-perf.pl | FlameGraph/flamegraph.pl > out.svg
    

6.3 实时监控命令

top -H -p <PID>        # 线程级CPU监控
cat /proc/<PID>/status # 查看线程数/内存
strace -f -p <PID>     # 跟踪系统调用

7. 面试常见问题
  • 进程 vs 线程的区别?

    • 进程:资源分配的最小单位,有独立地址空间(代码、数据、堆栈),切换开销大。
    • 线程:CPU调度的最小单位,共享进程资源(内存、文件描述符),切换开销小,通信高效。
      关键点:线程更轻量,适合嵌入式资源受限场景。
  • 互斥锁(mutex)与信号量(semaphore)的区别?

    特性 互斥锁 (mutex) 信号量 (semaphore)
    用途 保护共享资源 控制并发访问数量
    所有权 锁的持有者必须释放 任意线程可释放
    初始值 1(锁定/未锁定) 可设为N(资源数量)
    嵌入式注意 需避免优先级反转 适合生产者-消费者模型
  • 什么是优先级反转?如何解决?

    • 现象:低优先级线程持有锁,中优先级线程抢占,高优先级线程因等待锁被阻塞。
    • 解决方案
      • 优先级继承(Linux默认):持有锁的线程临时继承等待线程的最高优先级。
      • 优先级天花板:锁绑定一个优先级,持有锁的线程自动提升到该优先级。
      // 在pthread_mutexattr_t中设置优先级继承
      pthread_mutexattr_t attr;
      pthread_mutexattr_init(&attr);
      pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
      
  • 如何避免死锁?

    • 锁顺序:所有线程按固定顺序获取锁(如锁A→锁B)。
    • 超时机制pthread_mutex_timedlock() 避免无限等待。
    • 死锁检测:工具如Valgrind的Helgrind插件。
  • 自旋锁(spinlock) vs 互斥锁的适用场景?

    • 自旋锁:忙等待,适用于临界区执行时间极短(如<2个时间片)且多核环境。
    • 互斥锁:睡眠等待,适用于临界区较长或单核系统。
      注意:嵌入式实时系统(RTOS)中慎用自旋锁,可能破坏实时性。
  • pthread_create() 失败的可能原因?

    • 资源耗尽(线程数超过ulimit -u限制)
    • 内存不足(无法分配线程栈,默认约8MB)
    • 权限问题(嵌入式系统可能限制线程创建)
  • 如何设置线程栈大小?

    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setstacksize(&attr, 1024*128); // 设为128KB
    pthread_create(&tid, &attr, thread_func, NULL);
    
  • 如何设置线程优先级?

    struct sched_param param;
    param.sched_priority = 90; // 优先级值(1~99,越高越优先)
    pthread_attr_setschedpolicy(&attr, SCHED_FIFO); // 实时调度策略
    pthread_attr_setschedparam(&attr, &param);
    

    注意:需要root权限或CAP_SYS_NICE能力。

  • 如何定位多线程问题?

    • 日志追踪:添加线程ID打印(pthread_self())。
    • 工具
      • gdbthread apply all bt 查看所有线程堆栈。
      • strace -f:跟踪线程系统调用。
      • Valgrind:检测内存竞争(--tool=helgrind)。
  • 共享内存访问的优化方法?

    • 无锁编程:使用原子操作(atomic_inc())。
    • 线程局部存储__thread关键字避免竞争。
    • 减少锁粒度:分段锁(如哈希桶锁)。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐