04-线程与线程同步.md 90 KB


title: 线程与线程同步 tags: [

嵌入式Linux,
Linux应用编程,
多线程,
pthread,
线程同步,
互斥锁,
条件变量,
自旋锁,
读写锁,
生产者消费者,
IMX6ULL,

] created: 2026-09-18 updated: 2026-09-18

pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十一章 线程、第十二章 线程同步"

线程与线程同步

💡 关联知识:[[02-进程线程与IPC/01-信号机制]]、[[02-进程线程与IPC/02-进程管理]]、[[02-进程线程与IPC/03-进程间通信]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/9. pthread多线程基础]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/10. 互斥锁]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/11. 条件变量]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/12. 读写锁与自旋锁]];深入 → [[Linux+C+C++技术体系梳理/2. Linux系统编程/13. 线程池模式]]

进程让程序"能同时做多件事",线程则让同一个程序内部同时做多件事。进程切换开销大、进程间通信麻烦,而同一进程内的线程共享地址空间,切换便宜、通信容易,因此线程是现代并发程序的主力。但共享也带来代价:多个线程同时读写同一变量会引发竞态,必须用互斥锁、条件变量、自旋锁、读写锁等同步机制来保护。本篇把"线程基础"和"线程同步"合并在一起讲完,学完即可独立编写生产级多线程程序。


1. 线程基础

1.1 什么是线程

线程是参与系统调度的最小单位。它被包含在进程之中,是进程中的实际运行单位;一个线程是进程中一条单一顺序的控制流(执行路线),一个进程可以创建多个线程,多个线程并发运行、各执行不同任务。

启动一个程序时,操作系统创建一个进程,同时一个线程立刻运行,这个线程叫主线程main() 就是主线程的入口函数,main() 执行的任务就是主线程的任务。因此:

  • 任何一个进程都至少包含一个主线程,只有主线程的进程称为单线程进程
  • 除主线程外还包含其它线程的,称为多线程进程,其它线程通常由主线程调用 pthread_create() 创建,称为主线程的子线程

主线程的重要性体现在两方面:

  • 其它新线程(子线程)由主线程创建;
  • 主线程通常最后结束运行,负责各种清理工作,例如回收子线程。

线程的特点:

  • 线程不单独存在,而是包含在进程中;
  • 线程是参与系统调度的基本单位;
  • 可并发执行:同一进程的多个线程并发执行,宏观上表现为同时运行;
  • 共享进程资源:同一进程的各线程具有相同的地址空间,可访问进程已打开的文件、定时器、信号量等。

同时,每个线程又有私有的部分:各自的调用栈(线程栈)、寄存器环境(register context)、线程本地存储(thread-local storage)。

flowchart TB
    accTitle: 进程与线程的资源关系
    accDescr: 同一进程内多个线程共享代码段、数据段、堆和打开的文件,但各自拥有独立的线程栈和寄存器环境。
    subgraph P["进程(共享资源)"]
        CODE["代码段(共享,只读)"]
        DATA["数据段 / 堆(共享)"]
        FD["文件描述符表(共享)"]
        SIG["信号处理方式(共享)"]
        subgraph T1["线程 1"]
            S1["私有线程栈"]
            R1["私有寄存器环境"]
            L1["私有 TLS"]
        end
        subgraph T2["线程 2"]
            S2["私有线程栈"]
            R2["私有寄存器环境"]
            L2["私有 TLS"]
        end
        subgraph T3["线程 3"]
            S3["私有线程栈"]
            R3["私有寄存器环境"]
            L3["私有 TLS"]
        end
    end

线程也有自己的生命周期。新创建的线程进入就绪态,获得 CPU 调度后进入运行态;时间片耗尽或被更高优先级线程抢占时回到就绪态;执行到需要等待的操作(加锁、条件变量等待、sleep、阻塞 I/O 等)时进入阻塞态,条件满足或被唤醒后重新回到就绪态;执行 pthread_exit()、从 start 函数 return、或被取消请求在取消点生效时进入终止态

stateDiagram-v2
    accTitle: 线程生命周期状态
    accDescr: 线程从创建到终止的状态迁移,包含就绪、运行、阻塞等待与终止,取消请求需到达取消点才生效。
    [*] --> 就绪: pthread_create
    就绪 --> 运行: 获得CPU调度
    运行 --> 就绪: 时间片耗尽或被抢占
    运行 --> 阻塞: 加锁失败或wait或sleep或阻塞IO
    阻塞 --> 就绪: 条件满足或被唤醒
    运行 --> 终止: pthread_exit或return
    运行 --> 终止: 取消请求到达取消点
    终止 --> [*]

1.2 线程 vs 进程

创建多个子进程本质上就是多个单线程进程,同样能并发处理多任务。两种模型各有优劣:

多进程编程的劣势:

  • 进程间切换开销大。微观上仍是轮流切换,进程间切换开销远大于同一进程内多线程切换,对中小型程序不划算。
  • 进程间通信麻烦。每个进程处于各自独立、隔离的地址空间,相互通信较麻烦。

多线程编程的优势:

  • 同一进程的多个线程间切换开销小;
  • 同一进程的多个线程间通信容易(共享地址空间);
  • 线程创建速度远大于进程创建速度;
  • 多线程在多核处理器上更有优势。

多线程也有缺点:编程难度高,需要考虑线程安全、信号处理等问题,编写与调试比单线程困难。多进程模型通常用于大型应用程序(如网络服务器),中小型程序使用较少。

对比项 多进程 多线程
地址空间 各自独立、隔离 共享同一进程地址空间
切换开销
创建速度
通信方式 需要 IPC(管道、共享内存等) 直接读写全局变量即可
健壮性 一个进程崩溃不影响其它进程 一个线程崩溃可能拖垮整个进程
资源占用 每进程独立资源 共享进程资源,额外仅线程栈等
典型场景 网络服务器、大型系统 中小型并发程序、GUI、数据处理

1.3 并发、并行与串行

  • 串行:一件事、一件事接着做,只有一个执行单元;
  • 并发:交替做不同的事,同一个执行单元上把时间切成时间片,每个任务执行一段时间后切换,即时分复用
  • 并行:同时做不同的事,需要多个执行单元(多核)。

生活化比喻:

  • 吃饭吃到一半电话来了,吃完才去接——不支持并发也不支持并行,只是串行;
  • 停下吃饭去接电话,接完继续吃——支持并发;
  • 一边打电话一边吃饭——支持并行。

单核处理器(如 I.MX6ULL 的单核 Cortex-A7)只有一个执行单元,只能以并发方式运行系统中的线程;多核处理器可并行执行多个线程,且每个执行单元内部仍以并发方式调度多个线程。由于处理器速度极快,交替轮询一次的时间在宏观上几乎可忽略,所以"看起来"所有线程在同时运行。

1.4 线程 ID

每个进程有进程 ID(pid_t,非负整数),每个线程也有其标识,称为线程 ID。进程 ID 在整个系统中唯一,而线程 ID 只在所属进程上下文中才有意义。线程 ID 使用 pthread_t 数据类型表示。

函数 原型 参数 返回值 说明
pthread_self pthread_t pthread_self(void); 当前线程 ID 总是成功
pthread_equal int pthread_equal(pthread_t t1, pthread_t t2); 两个线程 ID 相等返回非 0,否则返回 0 跨平台比较线程 ID 的正确方式

在 Linux 下 pthread_t 实际是 unsigned long int,但其它系统不一定,所以应把 pthread_t 当作不透明类型,用 pthread_equal() 比较,而不是直接用 ==(调试打印时可以临时按其实际类型打印)。

线程 ID 的用途:很多线程函数(pthread_cancel()pthread_detach()pthread_join() 等)用线程 ID 标识目标线程;也可用特定线程 ID 作为动态数据结构的标签,标识数据结构的创建者或属主线程。

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

int main(void)
{
    pthread_t tid = pthread_self();
    printf("当前线程 ID<%lu>\n", (unsigned long)tid);
    return 0;
}

1.5 创建线程:pthread_create()

内容
原型 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
头文件 <pthread.h>
链接 -lpthread(pthread 不在 gcc 默认链接库中)
参数 thread pthread_t *,成功时新线程 ID 保存在此
参数 attr pthread_attr_t *,为 NULL 表示使用默认属性
参数 start_routine 函数指针,新线程从此函数开始运行,签名 void *(*)(void *)
参数 arg 传给 start_routine 的参数;可为 NULL
返回值 成功返回 0;失败返回错误号(不设置 errno

关键点:

  • start_routine 只能接收一个 void * 参数,需要传多个参数时应封装成结构体,把结构体地址作为 arg
  • arg 一般应指向全局变量或堆变量,保证在线程生命周期内该对象一直存在;若指向栈变量,线程访问时可能已失效;
  • pthread_create() 失败时返回错误码而不像系统调用那样设置 errno。每个线程有自己的一份 errno 副本(为兼容使用 errno 的函数),但线程函数中直接返回错误码更清晰,可把错误范围限制在出错函数内;
  • 线程创建成功后立即加入系统调度队列,获得 CPU 后从 start_routine() 开始运行。调用后无法确定系统先调度主线程还是新线程,若程序对执行顺序有强制要求,必须使用同步技术。

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    #include <string.h>
    #include <unistd.h>
    
    static void *new_thread_start(void *arg)
    {
    printf("新线程: 进程 ID<%d>  线程 ID<%lu>\n",
           getpid(), (unsigned long)pthread_self());
    return (void *)0;
    }
    
    int main(void)
    {
    pthread_t tid;
    int ret;
    
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "Error: %s\n", strerror(ret));
        exit(-1);
    }
    
    printf("主线程: 进程 ID<%d>  线程 ID<%lu>\n",
           getpid(), (unsigned long)pthread_self());
    
    sleep(1);   /* 主线程休眠,避免新线程还没运行进程就结束了 */
    exit(0);
    }
    

编译运行:

gcc -Wall -o testApp testApp.c -lpthread
./testApp

两个线程的进程 ID 相同(同属一个进程),线程 ID 不同;Linux 下线程 ID 数值很大,看起来像一个指针。

1.6 终止线程

终止线程的方式:

  • 线程的 start 函数执行 return 语句并返回指定值,返回值就是线程的退出码;
  • 线程调用 pthread_exit()
  • 调用 pthread_cancel() 取消线程;
  • 注意:如果进程中任意线程调用 exit()_exit()_Exit(),将导致整个进程终止。
内容
原型 void pthread_exit(void *retval);
头文件 <pthread.h>
参数 retval 线程返回值(退出码),可由 pthread_join() 获取
返回值

要点:

  • pthread_exit() 等价于在线程 start 函数中执行 return,区别是可在 start 函数所调用的任意函数中调用它来终止线程;
  • retval 所指向的内容不应分配在线程栈中(线程终止后栈内容是否有效不确定),start 函数的返回值同理;
  • 如果主线程调用 pthread_exit(),主线程终止,但其它线程仍正常运行,直到所有线程都终止进程才终止。

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    #include <unistd.h>
    
    static void *new_thread_start(void *arg)
    {
    printf("新线程 start\n");
    sleep(1);
    printf("新线程 end\n");
    pthread_exit(NULL);
    }
    
    int main(void)
    {
    pthread_t tid;
    int ret;
    
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "Error: %s\n", strerror(ret));
        exit(-1);
    }
    
    printf("主线程 end\n");
    pthread_exit(NULL);   /* 主线程退出,但进程不结束,新线程继续运行 */
    exit(0);
    }
    

1.7 回收线程:pthread_join()

类似父进程用 wait()/waitpid() 回收子进程,线程用 pthread_join() 阻塞等待线程终止并获取退出码、回收资源。

内容
原型 int pthread_join(pthread_t thread, void **retval);
头文件 <pthread.h>
参数 thread 需要等待的线程 ID
参数 retval NULL 时将目标线程退出状态复制到 *retval;若线程被取消,则 *retvalPTHREAD_CANCELED;不关心可传 NULL
返回值 成功返回 0;失败返回错误码

要点:

  • 调用 pthread_join() 会阻塞等待指定线程终止;若该线程已终止则立即返回;
  • 多个线程同时 pthread_join() 等待同一线程,结果不确定;
  • 若线程未分离,必须用 pthread_join() 回收;若线程终止后无人 join,该线程成为僵尸线程(与僵尸进程类似),浪费系统资源,积累过多将无法创建新线程。进程终止后,僵尸线程会被父进程回收;
  • waitpid() 的显著差别:

    • 线程之间关系对等,任意线程均可 pthread_join() 等待另一个线程(进程则是父进程唯一可 wait());
    • 不能以非阻塞方式调用 pthread_join()waitpid()WNOHANG)。

      #include <stdio.h>
      #include <stdlib.h>
      #include <pthread.h>
      #include <string.h>
      #include <unistd.h>
      
      static void *new_thread_start(void *arg)
      {
      printf("新线程 start\n");
      sleep(2);
      printf("新线程 end\n");
      pthread_exit((void *)10);
      }
      
      int main(void)
      {
      pthread_t tid;
      void *tret;
      int ret;
      
      ret = pthread_create(&tid, NULL, new_thread_start, NULL);
      if (ret) {
      fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
      exit(-1);
      }
      
      ret = pthread_join(tid, &tret);
      if (ret) {
      fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
      exit(-1);
      }
      printf("新线程终止, code=%ld\n", (long)tret);   /* 输出 10 */
      
      exit(0);
      }
      

1.8 取消线程:pthread_cancel()

有时需要向线程发送请求,要求它立刻退出,称为取消线程

内容
原型 int pthread_cancel(pthread_t thread);
头文件 <pthread.h>
参数 目标线程 ID
返回值 成功返回 0;失败返回错误码
说明 发出请求后立即返回,不等待目标线程退出

默认情况下目标线程也会立刻退出,行为如同调用了参数为 PTHREAD_CANCELED(即 (void *)-1)的 pthread_exit()。但线程可设置自己不被取消或控制如何被取消,因此 pthread_cancel() 只是提出请求

取消状态与取消类型

函数 原型
设置取消状态 int pthread_setcancelstate(int state, int *oldstate);
设置取消类型 int pthread_setcanceltype(int type, int *oldtype);

两者"设置新值 + 取回旧值"都是原子操作oldstate/oldtype 可为 NULL。成功返回 0,失败返回非 0 错误码。

state 取值:

  • PTHREAD_CANCEL_ENABLE:线程可被取消(新建线程与主线程的默认值);
  • PTHREAD_CANCEL_DISABLE:线程不可被取消,收到取消请求时将其挂起,直到状态变为 ENABLE。

type 取值(仅在状态为 ENABLE 时有效):

  • PTHREAD_CANCEL_DEFERRED:取消请求被挂起,直到线程到达某个取消点才响应(默认值);
  • PTHREAD_CANCEL_ASYNCHRONOUS:可能在任意时间点取消线程,应用场景很少。

当线程调用 fork() 创建子进程时,子进程会继承调用线程的取消状态和类型;调用 exec 函数时,新程序主线程的取消状态和类型重置为默认值(ENABLE + DEFERRED)。

取消点

取消点是一系列函数,只有执行到这些函数时才会真正响应取消请求。系统认为未到取消点时代码正在执行关键工作,不应被停止。常见取消点函数包括:

类别 函数
I/O read()write()open()close()pread()pwrite()fcntl()
睡眠 sleep()nanosleep()clock_nanosleep()pause()sigsuspend()
线程/信号 pthread_join()pthread_cond_wait()pthread_cond_timedwait()pthread_testcancel()sigwait()
等待/多路复用 wait()waitpid()select()poll()pselect()accept()connect()
IPC mq_send()mq_receive()sem_wait()msgrcv()msgsnd()
同步/文件 fsync()fdatasync()msync()system()tcdrain()

除这些函数外,不得将任何其它函数视为取消点(调用它们不会招致取消)。可用 man 7 pthreads 查看完整列表。

如果一个循环体不含取消点(如空 for(;;)),线程将永远无法被取消。此时可用 void pthread_testcancel(void); 手动产生一个取消点:若线程已有挂起的取消请求,调用该函数后线程立即终止。

取消示例

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

static void *new_thread_start(void *arg)
{
    printf("新线程--running\n");
    for ( ; ; )
        sleep(1);          /* sleep 是取消点,可被取消 */
    return (void *)0;
}

int main(void)
{
    pthread_t tid;
    void *tret;
    int ret;

    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    sleep(1);

    ret = pthread_cancel(tid);          /* 发送取消请求 */
    if (ret) {
        fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
        exit(-1);
    }

    ret = pthread_join(tid, &tret);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("新线程终止, code=%ld\n", (long)tret);   /* 输出 -1,即 PTHREAD_CANCELED */

    exit(0);
}

把 start 函数改成不含取消点的空循环,主线程将无法取消它;在循环中加 pthread_testcancel() 后又能被取消。

1.9 分离线程:pthread_detach()

默认线程终止时可由其它线程 pthread_join() 获取状态并回收资源。若不关心返回值、希望线程终止时系统自动回收,可将其分离

内容
原型 int pthread_detach(pthread_t thread);
头文件 <pthread.h>
参数 需要分离的线程 ID
返回值 成功返回 0;失败返回错误码

要点:

  • 线程既可以分离别的线程,也可以分离自己:pthread_detach(pthread_self());
  • 一旦处于分离状态,就不能再使用 pthread_join() 获取其终止状态,且此过程不可逆;
  • 处于分离状态的线程终止后,系统自动回收其资源。

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    #include <string.h>
    #include <unistd.h>
    
    static void *new_thread_start(void *arg)
    {
    int ret;
    
    ret = pthread_detach(pthread_self());   /* 自行分离 */
    if (ret) {
        fprintf(stderr, "pthread_detach error: %s\n", strerror(ret));
        return NULL;
    }
    
    printf("新线程 start\n");
    sleep(2);
    printf("新线程 end\n");
    pthread_exit(NULL);
    }
    
    int main(void)
    {
    pthread_t tid;
    int ret;
    
    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    
    sleep(1);
    
    ret = pthread_join(tid, NULL);          /* 已分离,必然失败 */
    if (ret)
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));  /* Invalid argument */
    
    pthread_exit(NULL);
    }
    

1.10 清理处理函数:pthread_cleanup_push/pop

类似进程用 atexit() 注册终止处理函数,线程退出时也可执行线程清理函数。与进程不同,一个线程可以注册多个清理函数,记录在清理函数栈中(先进后出),执行顺序与注册顺序相反,全部执行完线程才终止。

内容
原型 void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);
头文件 <pthread.h>
routine 清理函数指针,无返回值,只有一个 void * 参数
arg 调用清理函数 routine() 时传给它的参数
execute 0 只移除栈顶清理函数不执行;非 0 则移除并执行该函数

清理函数栈中的函数在以下情况被执行:

  • 线程调用 pthread_exit() 退出时;
  • 线程响应取消请求时;
  • 非 0参数调用 pthread_cleanup_pop() 时。

除此之外,其它终止方式不会执行清理函数,例如在线程 start 函数中执行 return 退出时不会执行。

注意:pthread_cleanup_push()pthread_cleanup_pop() 实际是,会展开为 {} 包裹的语句序列,因此必须在与线程相同的作用域中成对匹配使用,否则编译报错。

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

static void cleanup(void *arg)
{
    printf("cleanup: %s\n", (char *)arg);
}

static void *new_thread_start(void *arg)
{
    printf("新线程--start run\n");
    pthread_cleanup_push(cleanup, "第 1 次调用");
    pthread_cleanup_push(cleanup, "第 2 次调用");
    pthread_cleanup_push(cleanup, "第 3 次调用");

    sleep(2);
    pthread_exit((void *)0);        /* 触发清理函数栈 */

    /* 仅为与 pthread_cleanup_push 配对,否则编译不通过 */
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
}

int main(void)
{
    pthread_t tid;
    void *tret;
    int ret;

    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    ret = pthread_join(tid, &tret);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    printf("新线程终止, code=%ld\n", (long)tret);

    exit(0);
}

输出顺序为"第 3 次调用、第 2 次调用、第 1 次调用",验证了先进后出。若把 pthread_exit() 换成 return,则不会执行清理函数。

1.11 线程属性

创建线程时可通过 pthread_attr_t 设置属性;attrNULL 表示全部使用默认值。若自定义属性,需先用 pthread_attr_init() 初始化、用完用 pthread_attr_destroy() 销毁。

函数 原型
初始化/销毁属性 int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
栈地址与大小 int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
单独设置栈大小 int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
单独设置栈地址 int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);
int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr);
分离状态 int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);

detachstate 取值:

  • PTHREAD_CREATE_DETACHED:新建线程一开始便处于分离状态,结束后由系统回收资源,无法被 pthread_join()
  • PTHREAD_CREATE_JOINABLE:默认值,正常启动,可被其它线程获取终止状态。

设置栈大小为 4 KB 的示例:

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

static void *new_thread_start(void *arg)
{
    puts("Hello World!");
    return (void *)0;
}

int main(int argc, char *argv[])
{
    pthread_attr_t attr;
    pthread_t tid;
    int ret;

    pthread_attr_init(&attr);               /* 初始化属性对象 */
    pthread_attr_setstacksize(&attr, 4096); /* 设置栈大小为 4K */

    ret = pthread_create(&tid, &attr, new_thread_start, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    ret = pthread_join(tid, NULL);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }

    pthread_attr_destroy(&attr);            /* 销毁属性对象 */
    exit(0);
}

以分离状态启动线程:

pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, new_thread_start, NULL);
/* ... */
pthread_attr_destroy(&attr);

1.12 线程安全与可重入

线程栈

每个线程有自己独立的栈地址空间(线程栈),运行过程中的自动变量(局部变量)都分配在自己的线程栈中,互不干扰。创建线程时可配置栈大小与起始地址,多数情况保持默认即可。

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

static void *new_thread(void *arg)
{
    int number = *((int *)arg);
    unsigned long int tid = pthread_self();
    printf("当前为<%d>号线程, 线程 ID<%lu>\n", number, tid);
    return (void *)0;
}

static int nums[5] = {0, 1, 2, 3, 4};

int main(int argc, char *argv[])
{
    pthread_t tid[5];
    int j;

    for (j = 0; j < 5; j++)
        pthread_create(&tid[j], NULL, new_thread, &nums[j]);

    for (j = 0; j < 5; j++)
        pthread_join(tid[j], NULL);

    exit(0);
}

5 个线程使用同一个 start 函数,但每个线程的栈中各有一份 numbertid,互不影响。

可重入函数(Reentrant)

形成多条执行流有两种情况:一是多线程;二是信号处理——信号异步到来会打断主程序,从而在单线程进程内也形成主程序与信号处理函数两条执行流。

可重入函数:如果一个函数被同一进程的多个不同执行流同时调用,每次调用总能产生正确结果,则称为可重入函数。

可重入函数分两类:

  • 绝对可重入函数:无论怎么调用都可重入。特点是:函数内使用的变量均为局部变量(操作的内存地址均为本地栈地址);参数和返回值均为值类型;函数内调用的其它函数也都是绝对可重入函数。这类就是 pure code 可重入,多个副本使用分离的栈,互不干扰。
  • 带条件的可重入函数:满足某个/某些条件时才可重入。例如函数只读取全局变量而不修改它,可重入的前提是"多执行流调用期间该全局变量绝不被其它地方修改";参数为指针的函数,若每个执行流传入的都是自己本地变量的地址则安全,若传入共享变量地址则不安全。

反例(不可重入):

static int glob = 0;

static void func(int loops)
{
    int local;
    int j;

    for (j = 0; j < loops; j++) {
        local = glob;   /* 读全局变量 */
        local++;
        glob = local;   /* 写全局变量,多执行流并发将出错 */
    }
}

很多 C 库函数都有可重入版本,名称后面加 _r,如 asctime()/asctime_r()ctime()/ctime_r()localtime()/localtime_r()。可用 man 查看函数的 ATTRIBUTES 信息。

线程安全函数

线程安全函数:一个函数被多个线程(不包括信号处理函数产生的执行流)同时调用时,总能产生正确结果。

可重入函数一定是线程安全函数,但线程安全函数不一定是可重入函数——可重入函数是线程安全函数的真子集。区别在于:可重入只从语言语法角度分析(不涉及具体实现机制),而线程安全函数可以使用线程同步技术实现。例如把上面读写 glob 的函数加上互斥锁保护后,它变成线程安全函数,但因为修改了外部全局变量,仍不是可重入函数。

判断方法:用 man 手册查看 ATTRIBUTES,MT-Safe 表示线程安全、MT-Unsafe 表示线程不安全(MT = multithreaded)。POSIX.1-2001/2008 规定所有函数都必须是线程安全的,但有一批例外(asctimectimegmtimelocaltimerandstrerrorstrtokgetenvreaddirsystem 等)。man 7 pthreads 可查看完整列表。

一次性初始化:pthread_once()

多线程环境下,有些初始化代码段只能执行一次。用 pthread_once() 保证 init_routine() 仅执行一次(由内核调度决定在哪个线程执行)。

内容
原型 int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));
头文件 <pthread.h>
初始化 pthread_once_t once_control = PTHREAD_ONCE_INIT;
返回值 成功返回 0;失败返回错误编码

若一个线程调用 pthread_once() 时另一个线程也调用,后者会阻塞等待,直到第一个完成初始化返回。调用成功返回时,可确定所有状态都已初始化完成。

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

static pthread_once_t once = PTHREAD_ONCE_INIT;

static void initialize_once(void)
{
    printf("initialize_once 被执行: 线程 ID<%lu>\n", pthread_self());
}

static void func(void)
{
    pthread_once(&once, initialize_once);   /* 只会执行一次 */
    printf("函数 func 执行完毕.\n");
}

static void *thread_start(void *arg)
{
    printf("线程%d 被创建: 线程 ID<%lu>\n",
           *((int *)arg), pthread_self());
    func();
    pthread_exit(NULL);
}

static int nums[5] = {0, 1, 2, 3, 4};

int main(void)
{
    pthread_t tid[5];
    int j;

    for (j = 0; j < 5; j++)
        pthread_create(&tid[j], NULL, thread_start, &nums[j]);

    for (j = 0; j < 5; j++)
        pthread_join(tid[j], NULL);

    exit(0);
}

输出中 initialize_once() 只会被执行一次。

线程特有数据(TLS)

线程特有数据(线程私有数据)为每个调用线程维护一份变量副本,每个线程通过特有数据键(key) 访问时都拿到本线程绑定的副本,从而避免变量成为线程间共享数据。常用于把非线程安全函数改造为线程安全函数。

涉及 4 个函数:

函数 原型 说明
pthread_key_create int pthread_key_create(pthread_key_t *key, void (*destructor)(void*)); 创建 key,可绑定解构函数
pthread_setspecific int pthread_setspecific(pthread_key_t key, const void *value); 保存线程私有缓冲区指针并与 key、当前线程关联
pthread_getspecific void *pthread_getspecific(pthread_key_t key); 获取当前线程关联的私有缓冲区,未设置返回 NULL
pthread_key_delete int pthread_key_delete(pthread_key_t key); 删除 key(不触发解构函数,需先确保无线程使用)

pthread_key_create() 只需在第一个调用线程中创建一次,因此通常配合 pthread_once() 使用。当使用该 key 的线程终止时,绑定的 destructor() 会被自动调用以释放私有数据。

典型用法(把返回静态缓冲区的 strerror 改造为线程安全版):

#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>

#define MAX_ERROR_LEN 256

static pthread_once_t once = PTHREAD_ONCE_INIT;
static pthread_key_t strerror_key;

static void destructor(void *buf)
{
    free(buf);                  /* 释放线程私有缓冲区 */
}

static void create_key(void)
{
    if (pthread_key_create(&strerror_key, destructor))
        pthread_exit(NULL);
}

static char *my_strerror(int errnum)
{
    char *buf;

    if (pthread_once(&once, create_key))       /* 只创建一次 key */
        pthread_exit(NULL);

    buf = pthread_getspecific(strerror_key);   /* 取本线程私有缓冲区 */
    if (NULL == buf) {                          /* 首次调用,需分配 */
        buf = malloc(MAX_ERROR_LEN);
        if (NULL == buf)
            pthread_exit(NULL);
        if (pthread_setspecific(strerror_key, buf))
            pthread_exit(NULL);
    }

    if (errnum < 0 || errnum >= _sys_nerr || NULL == _sys_errlist[errnum])
        snprintf(buf, MAX_ERROR_LEN, "Unknown error %d", errnum);
    else {
        strncpy(buf, _sys_errlist[errnum], MAX_ERROR_LEN - 1);
        buf[MAX_ERROR_LEN - 1] = '\0';
    }

    return buf;
}

⚠️ 来源说明_GNU_SOURCE_sys_errlist_sys_nerr 属于 GNU 扩展,用于演示;实际项目中更推荐使用可重入的 strerror_r()

线程局部存储:__thread

线程局部存储比线程特有数据更简单:在全局或静态变量声明时加 __thread 修饰符,每个线程就都拥有一份该变量的拷贝,一直存在到线程终止时自动释放。

static __thread char buf[512];

注意事项:

  • 变量声明中若用了 staticextern__thread 必须紧随其后;
  • 与一般全局/静态变量一样,声明时可设初始值;
  • 可用取值操作符 & 获取线程局部变量地址。

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <pthread.h>
    
    static __thread char buf[100];      /* 每个线程一份拷贝 */
    
    static void *thread_start(void *arg)
    {
    strcpy(buf, "Child Thread\n");
    printf("子线程: buf (%p) = %s", buf, buf);
    pthread_exit(NULL);
    }
    
    int main(int argc, char *argv[])
    {
    pthread_t tid;
    int ret;
    
    strcpy(buf, "Main Thread\n");
    
    if (ret = pthread_create(&tid, NULL, thread_start, NULL)) {
        fprintf(stderr, "pthread_create error: %d\n", ret);
        exit(-1);
    }
    
    if (ret = pthread_join(tid, NULL)) {
        fprintf(stderr, "pthread_join error: %d\n", ret);
        exit(-1);
    }
    
    printf("主线程: buf (%p) = %s", buf, buf);
    exit(0);
    }
    

主线程和子线程打印出的 buf 地址不同,证明它们操作的是各自线程的副本。

1.13 线程与信号

Linux 信号模型基于进程设计,信号问世远早于线程,因此两者结合使用较为复杂。以下是信号在多线程下的行为映射规则:

  • 系统默认行为属于进程层面:任一线程收到未经处理的信号,都会执行该信号的默认动作(通常停止或终止进程);
  • 信号处理函数属于进程层面:进程中所有线程共享注册的信号处理函数;
  • 信号发送既可针对整个进程,也可针对特定线程。以下三种情况信号针对某个线程:
    • 硬件异常信号(SIGBUSSIGFPESIGILLSIGSEGV)由某个线程执行指令引起,系统将其发送给该线程;
    • 线程对已断开的管道写操作产生的 SIGPIPE 信号;
    • pthread_kill()pthread_sigqueue() 发出的信号。
    • 除此之外(如其它进程 kill()/sigqueue()、终端 Ctrl+C 产生的 SIGINT 等)均属于进程层面;
  • 多线程进程收到绑定了处理函数的信号时,内核会任选一个线程来处理,并非每个线程都收到并处理;
  • 信号掩码属于线程层面:多线程下不存在作用于整个进程的信号掩码,各线程可独立阻止或放行信号;
  • 内核分别维护"针对整个进程挂起的信号"和"针对每个线程挂起的信号",sigpending() 返回两者的并集。

线程的信号掩码

单线程程序用 sigprocmask() 设置进程信号掩码;多线程用 pthread_sigmask() 设置各线程的信号掩码,用法与 sigprocmask() 完全一样。

内容
原型 int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
头文件 <signal.h>

每个新建线程会从其创建者处继承信号掩码,之后可调用 pthread_sigmask() 改变自己的掩码。

向线程发送信号

函数 原型 说明
pthread_kill int pthread_kill(pthread_t thread, int sig); 向同一进程中的指定线程发送信号;sig 为 0 时不发送信号但仍做错误检查
pthread_sigqueue int pthread_sigqueue(pthread_t thread, int sig, const union sigval value); sigqueue() 类似,但向指定线程发送并携带伴随数据

kill()/sigqueue() 针对整个进程;pthread_kill()/pthread_sigqueue() 针对某个线程。

异步信号安全函数

异步信号安全函数:可以在信号处理函数中被安全调用的线程安全函数,要求比线程安全函数更严格。可重入函数一定是异步信号安全函数,线程安全函数则不一定是。

例如一个用互斥锁保护的线程安全函数,若信号处理函数中再次调用它,而处理信号的正是刚持有锁的线程,就会死锁。要将其实现为异步信号安全函数,可在获取锁之前通过设置信号掩码禁止接收该信号(将函数实现为不可被信号中断)。

常见异步信号安全函数(部分):_exitopenclosereadwriteforkexecvekillwaitwaitpidsigactionsigprocmasksigqueuepipesocketconnectacceptpthread_killpthread_selfpthread_sigmask 等。可用 man 7 signal 查看完整列表。

一个安全的信号处理函数应满足:

  • 处理函数本身代码可重入,且只调用异步信号安全函数;
  • 主程序执行不安全函数、或操作信号处理函数也会更新的全局数据结构时,阻塞信号传递。

注意:本书示例在信号处理函数中调用了非异步信号安全的 printf() 仅为了打印方便,实际项目中不应这样用。


2. 线程同步

2.1 为什么需要线程同步

线程同步是为了对共享资源的访问进行保护,解决数据一致性问题。

  • 若每个线程访问的变量都是其它线程不会读写的(如各自局部变量、只有一个线程访问的全局变量),不存在一致性问题;
  • 变量只读时,多线程同时读取也没有问题;
  • 当一个线程可修改的变量,其它线程也能读取或修改时,就存在一致性问题,需要同步。

本质原因是多个线程并发访问共享资源形成竞态(race),可能导致读写到无效值。例如线程 A 读变量、再写新值,写操作需两个时钟周期,线程 B 在中间读取,就会读到不一致的值。

用两个线程各递增全局变量 1000 万次来说明:期望结果 2000 万,实际结果往往小于它。

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

static int g_count = 0;

static void *new_thread_start(void *arg)
{
    int loops = *((int *)arg);
    int l_count, j;

    for (j = 0; j < loops; j++) {
        l_count = g_count;      /* 读全局变量到本地 */
        l_count++;              /* 本地递增 */
        g_count = l_count;      /* 写回全局变量 */
    }

    return (void *)0;
}

static int loops;
int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;
    int ret;

    if (2 > argc)
        loops = 10000000;       /* 默认 1000 万次 */
    else
        loops = atoi(argv[1]);

    ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    ret = pthread_join(tid1, NULL);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }

    ret = pthread_join(tid2, NULL);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }

    printf("g_count = %d\n", g_count);
    exit(0);
}

传参 1000 时结果正确(2000),传默认 1000 万时结果通常小于 2000 万,这就是数据不一致。

flowchart LR
    accTitle: 并发访问共享变量导致数据不一致
    accDescr: 两个线程交错读写同一全局变量,线程B在写操作未完成时读取,得到不一致的值。
    subgraph A["线程 A"]
        A1["读 g_count = 5"] --> A2["写 6(需 2 个时钟周期)"]
    end
    subgraph B["线程 B"]
        B1["在写周期中间读 g_count"] --> B2["读到 5,而非 6"]
    end
    A2 -.->|"中间被读取"| B1

Linux 提供了多种线程同步机制:互斥锁、条件变量、自旋锁、读写锁等。

2.2 互斥锁(Mutex)

互斥锁(mutex,互斥量)本质是一把锁:访问共享资源前上锁,访问完成后解锁。上锁后任何其它试图再次加锁的线程都会被阻塞,直到当前线程释放锁。若有多个线程阻塞等待,解锁后它们会竞争,只有一个能成功上锁,其余继续阻塞。

程序设计的一个前提:所有访问共享资源的线程都必须遵守相同的加锁规则。若允许某个线程不拿锁就访问共享资源,即使其它线程都加锁,仍会出现数据不一致。

2.2.1 初始化

互斥锁使用 pthread_mutex_t 类型,两种初始化方式:

方式一:宏 PTHREAD_MUTEX_INITIALIZER(只适用于定义时直接初始化)。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

方式二:pthread_mutex_init()(适用于先定义后初始化、或堆中动态分配)。

内容
原型 int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
头文件 <pthread.h>
参数 mutex 待初始化的互斥锁
参数 attr 互斥锁属性;NULL 表示默认属性(等价于宏方式,区别是宏方式不做错误检查)
返回值 成功返回 0;失败返回非 0 错误码
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);

/* 或堆上分配 */
pthread_mutex_t *mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(mutex, NULL);

2.2.2 加锁与解锁

函数 原型 说明 返回值
pthread_mutex_lock int pthread_mutex_lock(pthread_mutex_t *mutex); 未锁定则上锁并返回;已被锁定则阻塞直到解锁 成功 0,失败非 0
pthread_mutex_trylock int pthread_mutex_trylock(pthread_mutex_t *mutex); 未锁定则上锁返回;已锁定则不阻塞,返回 EBUSY 成功 0,失败非 0(EBUSY
pthread_mutex_unlock int pthread_mutex_unlock(pthread_mutex_t *mutex); 解锁 成功 0,失败非 0

以下行为均属错误:

  • 对处于未锁定状态的互斥锁解锁;
  • 解锁由其它线程锁定的互斥锁。

多个线程阻塞等待时,解锁后哪个线程先上锁无法判断。

用互斥锁保护前面的 g_count 示例(默认 1000 万次也能得到正确结果):

static pthread_mutex_t mutex;
static int g_count = 0;

static void *new_thread_start(void *arg)
{
    int loops = *((int *)arg);
    int l_count, j;

    for (j = 0; j < loops; j++) {
        pthread_mutex_lock(&mutex);     /* 上锁 */

        l_count = g_count;
        l_count++;
        g_count = l_count;

        pthread_mutex_unlock(&mutex);   /* 解锁 */
    }

    return (void *)0;
}

/* main 中创建线程前初始化:pthread_mutex_init(&mutex, NULL); */

以非阻塞方式加锁(效果相同,但会忙等):

while (pthread_mutex_trylock(&mutex));   /* 轮询直到拿到锁 */
/* 访问共享资源 */
pthread_mutex_unlock(&mutex);

互斥锁的加锁、临界区、解锁与唤醒竞争关系如下:

flowchart TD
    accTitle: 互斥锁加锁解锁流程
    accDescr: 线程访问共享资源前先加锁,锁空闲则进入临界区并在退出时解锁,锁被占用则阻塞等待,解锁后唤醒等待线程竞争。
    A["访问共享资源前"] --> B["pthread_mutex_lock"]
    B --> C{"锁是否空闲"}
    C -->|是| D["加锁成功,进入临界区"]
    C -->|否| E["阻塞休眠,等待锁释放"]
    E --> C
    D --> F["访问共享资源"]
    F --> G["pthread_mutex_unlock"]
    G --> H["唤醒其它等待线程竞争"]

2.2.3 销毁

内容
原型 int pthread_mutex_destroy(pthread_mutex_t *mutex);
返回值 成功返回 0;失败返回非 0 错误码
  • 不能销毁还没有解锁的互斥锁;
  • 没有初始化的互斥锁也不能销毁;
  • 销毁后不能再上锁/解锁,需重新 pthread_mutex_init() 后才能使用。

2.2.4 死锁

  • 同一线程对同一互斥锁加锁两次,会陷入死锁;
  • 更隐蔽的情况:一个线程需要同时访问两个由不同互斥锁保护的资源,多个线程以不同顺序加锁时可能死锁。

    /* 线程 A */                    /* 线程 B */
    pthread_mutex_lock(mutex1);     pthread_mutex_lock(mutex2);
    pthread_mutex_lock(mutex2);     pthread_mutex_lock(mutex1);
    

两个线程互相请求对方持有的锁,都无法向前运行。

避免方法:

  1. 定义锁的层级关系,所有线程总是按相同顺序对一组互斥锁加锁(如都先锁 mutex1 再锁 mutex2);
  2. 当顺序难确定时,先 pthread_mutex_lock() 锁定第一个锁,再用 pthread_mutex_trylock() 逐个尝试锁定其余锁;任一 trylock 失败(返回 EBUSY)就释放所有锁,过一段时间重试。这种方式效率较低,因为可能多次循环。

2.2.5 互斥锁属性与类型

pthread_mutexattr_t 定义互斥锁属性。使用非默认属性时,参数 attr 必须指向已初始化的属性对象。

函数 原型
初始化/销毁属性 int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
获取/设置类型 int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

4 种类型:

类型 行为
PTHREAD_MUTEX_NORMAL 标准类型,不做错误检查或死锁检测;同一线程重复加锁会死锁;对未锁定或由其它线程锁定的锁解锁结果不确定
PTHREAD_MUTEX_ERRORCHECK 提供错误检查:同一线程重复加锁、解锁他人锁定的锁、解锁未锁定的锁都会返回错误;运行较慢,可作调试工具
PTHREAD_MUTEX_RECURSIVE 递归锁,允许同一线程在解锁前多次加锁并维护加锁次数;解锁次数必须等于加锁次数才真正释放
PTHREAD_MUTEX_DEFAULT 默认行为;用 PTHREAD_MUTEX_INITIALIZERattrNULL 初始化的锁属于此类,Linux 下行为与 NORMAL 相仿

使用方式:

pthread_mutex_t mutex;
pthread_mutexattr_t attr;

pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
pthread_mutex_init(&mutex, &attr);

/* ... 使用 ... */

pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&mutex);

2.3 条件变量(Condition Variable)

条件变量用于自动阻塞线程,直到某个特定事件发生或条件满足。它通常与互斥锁搭配使用,包含两个动作:

  • 一个线程等待某个条件满足而被阻塞;
  • 另一个线程在条件满足时发出"信号"。

这里的"信号"指条件变量的通知,与第八章的 Linux 信号不是一回事。

条件变量不保存状态信息,只是传递应用程序状态信息的一种通讯机制。若在没有任何线程等待时发送信号,该信号会不了了之。

条件变量必须与互斥锁配合:因为条件检测通常需要访问共享资源,条件本身由互斥锁保护,线程改变条件状态前必须先锁住互斥锁。

2.3.1 初始化与销毁

函数 原型
初始化 int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
销毁 int pthread_cond_destroy(pthread_cond_t *cond);

也可用宏 PTHREAD_COND_INITIALIZER 初始化:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

attrNULL 表示默认属性(等价于宏方式)。成功返回 0,失败返回非 0 错误码。

注意事项:

  • 使用前必须初始化;
  • 对已初始化的条件变量再次初始化可能导致未定义行为;
  • 对未初始化的条件变量销毁可能导致未定义行为;
  • 仅当没有任何线程等待它时,销毁才是最安全的;
  • 销毁后可以再次 pthread_cond_init() 重新初始化。

2.3.2 通知与等待

函数 原型 说明
pthread_cond_signal int pthread_cond_signal(pthread_cond_t *cond); 至少唤醒一个等待线程,更高效
pthread_cond_broadcast int pthread_cond_broadcast(pthread_cond_t *cond); 唤醒所有等待线程,总能得到正确结果
pthread_cond_wait int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); 阻塞等待,直到收到通知;成功 0,失败非 0

要点:

  • pthread_cond_signal()pthread_cond_broadcast() 调用成功返回 0,失败返回非 0;
  • pthread_cond_wait() 内部会对 mutex 操作:调用前线程必须已持有互斥锁;函数原子地把线程加入等待列表并解锁互斥锁;被唤醒返回时会再次锁住互斥锁
  • pthread_cond_broadcast() 唤醒所有线程时,互斥锁也只能被某一线程锁住,其它线程获取锁失败又会陷入阻塞。

2.3.3 判断条件为什么用 while 而不是 if

调用 pthread_cond_wait() 返回后,并不能确定判断条件是真还是假,必须立即重新检查;若条件不满足,继续休眠等待。因此必须用 while 循环:

while (0 >= g_avail)
    pthread_cond_wait(&cond, &mutex);

原因:

  • 多个线程等待同一条件时,任何线程都可能率先醒来获取互斥锁,并可能修改共享变量从而改变条件状态;
  • 可能存在虚假通知

2.3.4 条件变量的属性

条件变量有两个属性:进程共享属性和时钟属性,各有 get/set 方法,本书不深入介绍。

2.3.5 条件变量示例

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

static pthread_mutex_t mutex;   /* 定义互斥锁 */
static pthread_cond_t cond;     /* 定义条件变量 */
static int g_avail = 0;         /* 全局共享资源 */

/* 消费者线程 */
static void *consumer_thread(void *arg)
{
    for ( ; ; ) {
        pthread_mutex_lock(&mutex);         /* 上锁 */

        while (0 >= g_avail)                /* 用 while 重新检查条件 */
            pthread_cond_wait(&cond, &mutex);

        while (0 < g_avail)
            g_avail--;                      /* 消费 */

        pthread_mutex_unlock(&mutex);       /* 解锁 */
    }

    return (void *)0;
}

/* 主线程(生产者) */
int main(int argc, char *argv[])
{
    pthread_t tid;
    int ret;

    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);

    ret = pthread_create(&tid, NULL, consumer_thread, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    for ( ; ; ) {
        pthread_mutex_lock(&mutex);         /* 上锁 */
        g_avail++;                          /* 生产 */
        pthread_mutex_unlock(&mutex);       /* 解锁 */
        pthread_cond_signal(&cond);         /* 通知消费者 */
    }

    exit(0);
}

消费者判断 g_avail <= 0 时调用 pthread_cond_wait() 阻塞并解锁;生产者 g_avail++ 后解锁并 pthread_cond_signal() 唤醒消费者,消费者被唤醒后自动重新加锁并消费。

sequenceDiagram
    accTitle: 互斥锁与条件变量协作流程
    accDescr: 消费者在条件不满足时等待,生产者生产后发信号唤醒,wait 原子解锁并在返回时重新加锁。
    participant C as 消费者线程
    participant M as 互斥锁
    participant V as 条件变量
    participant P as 生产者线程
    C->>M: pthread_mutex_lock
    C->>V: g_avail<=0 时 pthread_cond_wait
    Note over C,M: wait 原子地解锁并阻塞
    P->>M: pthread_mutex_lock
    P->>P: g_avail++ 生产
    P->>M: pthread_mutex_unlock
    P->>V: pthread_cond_signal
    V-->>C: 唤醒,wait 重新加锁返回
    C->>C: 重新检查条件并消费
    C->>M: pthread_mutex_unlock

2.3.6 超时等待:pthread_cond_timedwait()

pthread_cond_wait() 会无限期等待。若希望等待一段有上限的时间,可使用 pthread_cond_timedwait(),超时后返回错误码 ETIMEDOUT

⚠️ 来源说明pthread_cond_timedwait() 在《I.MX6U嵌入式Linux C应用编程指南》中仅作为"取消点函数"被列出,未展开讲解其原型与用法,以下为 POSIX 标准接口的扩展补充。

内容
原型 int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
参数 abstime 绝对时间(从 Epoch 起的秒 + 纳秒),不是相对时间
返回值 成功 0;超时返回 ETIMEDOUT;其它错误返回相应错误码
#include <time.h>
#include <errno.h>

struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);   /* 取当前时间 */
ts.tv_sec += 3;                        /* 绝对超时时间:当前 +3 秒 */

pthread_mutex_lock(&mutex);
while (0 >= g_avail) {
    if (ETIMEDOUT == pthread_cond_timedwait(&cond, &mutex, &ts)) {
        /* 超时,做相应处理 */
        break;
    }
}
pthread_mutex_unlock(&mutex);

注意 abstime 是绝对时间,必须先取当前时间再加上超时时长。

2.4 自旋锁(Spinlock)

自旋锁与互斥锁相似,本质也是一把锁。区别在于:

  • 互斥锁获取不到时线程陷入阻塞(休眠),直到获取到锁被唤醒;
  • 自旋锁获取不到时在原地自旋(循环查看锁的持有者是否释放),一直占用 CPU。

实现上,互斥锁基于自旋锁实现,所以自旋锁更底层。自旋锁的不足是:未获得锁时一直运行、占着 CPU,若不能很快获得锁会降低 CPU 效率。

试图对同一自旋锁加锁两次必然死锁;而同一互斥锁加锁两次不一定死锁(如 ERRORCHECK 类型会返回错误)。

自旋锁适用于需要保护的临界区执行时间很短的场景:持锁线程很快释放锁,自旋等待的线程只需等待很短时间,效率高。

对比项 互斥锁 自旋锁
实现层次 基于自旋锁实现,更上层 更底层
获取失败行为 阻塞休眠,唤醒后竞争 原地自旋,忙等
开销 休眠/唤醒开销大 忙等占用 CPU,但切换开销小
适用场景 临界区可能较长 临界区极短
中断上下文 不能用于中断服务函数 可用于中断服务函数,内核中会自动禁止抢占

2.4.1 初始化与销毁

函数 原型
初始化 int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
销毁 int pthread_spin_destroy(pthread_spinlock_t *lock);

pshared 取值:

  • PTHREAD_PROCESS_SHARED:共享自旋锁,可在多个进程的线程间共享;
  • PTHREAD_PROCESS_PRIVATE:私有自旋锁,只有本进程内的线程可用。

成功返回 0,失败返回非 0 错误码。

2.4.2 加锁与解锁

函数 原型 说明
pthread_spin_lock int pthread_spin_lock(pthread_spinlock_t *lock); 未锁定则上锁;已锁定则自旋等待
pthread_spin_trylock int pthread_spin_trylock(pthread_spinlock_t *lock); 获取不到立即返回 EBUSY,不自旋
pthread_spin_unlock int pthread_spin_unlock(pthread_spinlock_t *lock); 解锁

成功返回 0,失败返回非 0 错误码。

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

static pthread_spinlock_t spin;     /* 定义自旋锁 */
static int g_count = 0;

static void *new_thread_start(void *arg)
{
    int loops = *((int *)arg);
    int l_count, j;

    for (j = 0; j < loops; j++) {
        pthread_spin_lock(&spin);       /* 自旋锁上锁 */

        l_count = g_count;
        l_count++;
        g_count = l_count;

        pthread_spin_unlock(&spin);     /* 自旋锁解锁 */
    }

    return (void *)0;
}

static int loops;
int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;
    int ret;

    if (2 > argc)
        loops = 10000000;
    else
        loops = atoi(argv[1]);

    pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);

    ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    printf("g_count = %d\n", g_count);
    pthread_spin_destroy(&spin);
    exit(0);
}

对比可见,替换为自旋锁后程序运行耗时明显变短,但要注意使用场景。

2.5 读写锁(Read-Write Lock)

互斥锁和自旋锁只有"加锁/不加锁"两种状态,一次只有一个线程能加锁。读写锁有 3 种状态:

  • 读模式下的加锁状态(读加锁);
  • 写模式下的加锁状态(写加锁);
  • 不加锁状态。

一次只有一个线程可以占有写模式的读写锁,但可以有多个线程同时占有读模式的读写锁,因此读写锁比互斥锁并行性更高。读写锁也称共享互斥锁:读模式锁住称为共享模式锁住,写模式锁住称为互斥模式锁住。

两条规则:

  • 读写锁处于写加锁状态时,解锁前所有试图加锁的线程(无论读模式还是写模式)都被阻塞;
  • 读写锁处于读加锁状态时,所有以读模式加锁的线程都能成功;任何以写模式加锁的线程都被阻塞,直到所有读模式锁被释放。

因此读写锁非常适合读的次数远大于写的次数的场景。

2.5.1 初始化与销毁

函数 原型
初始化 int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
销毁 int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

也可用宏 PTHREAD_RWLOCK_INITIALIZER

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

成功返回 0,失败返回非 0 错误码。

2.5.2 上锁与解锁

函数 原型 说明
pthread_rwlock_rdlock int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); 以读模式加锁
pthread_rwlock_wrlock int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); 以写模式加锁
pthread_rwlock_unlock int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); 解锁(读/写模式通用)
pthread_rwlock_tryrdlock int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); 非阻塞读加锁,失败返回 EBUSY
pthread_rwlock_trywrlock int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); 非阻塞写加锁,失败返回 EBUSY
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

static pthread_rwlock_t rwlock;     /* 定义读写锁 */
static int g_count = 0;

static void *read_thread(void *arg)
{
    int number = *((int *)arg);
    int j;

    for (j = 0; j < 10; j++) {
        pthread_rwlock_rdlock(&rwlock);             /* 读模式获取锁 */
        printf("读线程<%d>, g_count=%d\n", number + 1, g_count);
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }

    return (void *)0;
}

static void *write_thread(void *arg)
{
    int number = *((int *)arg);
    int j;

    for (j = 0; j < 10; j++) {
        pthread_rwlock_wrlock(&rwlock);             /* 写模式获取锁 */
        printf("写线程<%d>, g_count=%d\n", number + 1, g_count += 20);
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }

    return (void *)0;
}

static int nums[5] = {0, 1, 2, 3, 4};
int main(int argc, char *argv[])
{
    pthread_t tid[10];
    int j;

    pthread_rwlock_init(&rwlock, NULL);

    for (j = 0; j < 5; j++)
        pthread_create(&tid[j], NULL, read_thread, &nums[j]);

    for (j = 0; j < 5; j++)
        pthread_create(&tid[j + 5], NULL, write_thread, &nums[j]);

    for (j = 0; j < 10; j++)
        pthread_join(tid[j], NULL);

    pthread_rwlock_destroy(&rwlock);
    exit(0);
}

2.5.3 读写锁属性

读写锁只有一个属性——进程共享属性,与互斥锁、自旋锁相同。

函数 原型
初始化/销毁属性 int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
获取/设置共享属性 int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);

pshared 取值:

  • PTHREAD_PROCESS_SHARED:共享读写锁,可在多个进程的线程间共享;
  • PTHREAD_PROCESS_PRIVATE:私有读写锁,只有本进程内线程可用(默认值)。

    pthread_rwlock_t rwlock;
    pthread_rwlockattr_t attr;
    
    pthread_rwlockattr_init(&attr);
    pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_PRIVATE);
    pthread_rwlock_init(&rwlock, &attr);
    
    /* ... 使用 ... */
    
    pthread_rwlock_destroy(&rwlock);
    pthread_rwlockattr_destroy(&attr);
    

2.6 各同步机制对比

机制 类型 状态数 获取失败行为 适用场景
互斥锁 pthread_mutex_t 2(锁/未锁) 阻塞休眠 一般临界区保护,通用
条件变量 pthread_cond_t 无状态(配合互斥锁) 阻塞等待通知 等待某个条件成立(生产者-消费者)
自旋锁 pthread_spinlock_t 2(锁/未锁) 原地自旋忙等 临界区极短;可用于中断上下文
读写锁 pthread_rwlock_t 3(读锁/写锁/未锁) 读-读并发,读-写/写-写阻塞 读多写少

选择建议:实际开发中用得最多的是互斥锁和条件变量;临界区极短且不希望休眠时用自旋锁;读多写少时用读写锁。应根据场景选择,方能事半功倍。

2.7 生产者-消费者模型完整示例

下面把互斥锁与条件变量组合成一个完整的、可直接编译运行的生产者-消费者程序,使用一个固定容量的环形缓冲区,支持多个生产者与多个消费者。

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

#define BUFFER_SIZE     8       /* 缓冲区容量 */
#define PRODUCER_NUM    2       /* 生产者数量 */
#define CONSUMER_NUM    3       /* 消费者数量 */
#define ITEM_TOTAL      20      /* 生产者各自生产的总数 */

static pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; /* 保护缓冲区 */
static pthread_cond_t  g_not_full  = PTHREAD_COND_INITIALIZER; /* 缓冲区非满 */
static pthread_cond_t  g_not_empty = PTHREAD_COND_INITIALIZER; /* 缓冲区非空 */

static int g_buffer[BUFFER_SIZE];   /* 环形缓冲区 */
static int g_in = 0;                /* 生产写入位置 */
static int g_out = 0;               /* 消费读出位置 */
static int g_count = 0;             /* 当前元素个数 */

static int g_stop = 0;              /* 生产结束标志 */

/* 向缓冲区放入元素 */
static void buffer_put(int value)
{
    pthread_mutex_lock(&g_mutex);

    while (g_count == BUFFER_SIZE)                  /* 缓冲区满则等待 */
        pthread_cond_wait(&g_not_full, &g_mutex);

    g_buffer[g_in] = value;
    g_in = (g_in + 1) % BUFFER_SIZE;
    g_count++;

    printf("生产者: 放入 %d, 当前元素 %d\n", value, g_count);

    pthread_cond_signal(&g_not_empty);              /* 通知消费者非空 */
    pthread_mutex_unlock(&g_mutex);
}

/* 从缓冲区取出元素,成功返回 0,无数据且已结束返回 -1 */
static int buffer_get(int *value)
{
    int ret = 0;

    pthread_mutex_lock(&g_mutex);

    while (0 == g_count && !g_stop)                 /* 空且未结束则等待 */
        pthread_cond_wait(&g_not_empty, &g_mutex);

    if (0 == g_count && g_stop) {                   /* 已结束且无数据 */
        ret = -1;
    } else {
        *value = g_buffer[g_out];
        g_out = (g_out + 1) % BUFFER_SIZE;
        g_count--;

        printf("消费者: 取出 %d, 当前元素 %d\n", *value, g_count);

        pthread_cond_signal(&g_not_full);           /* 通知生产者非满 */
    }

    pthread_mutex_unlock(&g_mutex);
    return ret;
}

/* 生产者线程 */
static void *producer_thread(void *arg)
{
    int id = *((int *)arg);
    int i;

    for (i = 0; i < ITEM_TOTAL; i++) {
        buffer_put(id * 100 + i);
        usleep(1000);                               /* 模拟生产耗时 */
    }

    pthread_exit(NULL);
}

/* 消费者线程 */
static void *consumer_thread(void *arg)
{
    int value;

    (void)arg;
    for ( ; ; ) {
        if (0 != buffer_get(&value))                /* 结束且无数据则退出 */
            break;
        usleep(2000);                               /* 模拟消费耗时 */
    }

    pthread_exit(NULL);
}

int main(void)
{
    pthread_t pid[PRODUCER_NUM];
    pthread_t cid[CONSUMER_NUM];
    int pnum[PRODUCER_NUM];
    int i;

    /* 创建生产者线程 */
    for (i = 0; i < PRODUCER_NUM; i++) {
        pnum[i] = i + 1;
        if (pthread_create(&pid[i], NULL, producer_thread, &pnum[i])) {
            fprintf(stderr, "create producer error\n");
            exit(-1);
        }
    }

    /* 创建消费者线程 */
    for (i = 0; i < CONSUMER_NUM; i++) {
        if (pthread_create(&cid[i], NULL, consumer_thread, NULL)) {
            fprintf(stderr, "create consumer error\n");
            exit(-1);
        }
    }

    /* 等待生产者结束 */
    for (i = 0; i < PRODUCER_NUM; i++)
        pthread_join(pid[i], NULL);

    /* 通知消费者生产已结束 */
    pthread_mutex_lock(&g_mutex);
    g_stop = 1;
    pthread_cond_broadcast(&g_not_empty);           /* 唤醒所有消费者 */
    pthread_mutex_unlock(&g_mutex);

    /* 等待消费者结束 */
    for (i = 0; i < CONSUMER_NUM; i++)
        pthread_join(cid[i], NULL);

    pthread_mutex_destroy(&g_mutex);
    pthread_cond_destroy(&g_not_full);
    pthread_cond_destroy(&g_not_empty);

    printf("全部线程结束\n");
    return 0;
}

逐段解释

代码 说明
g_mutex / g_not_full / g_not_empty 一个互斥锁保护缓冲区状态,两个条件变量分别表示"非满"和"非空"
g_in / g_out / g_count 环形缓冲区的写指针、读指针、元素个数,全部在互斥锁保护下修改
buffer_put() 进入时加锁;用 while 检查缓冲区是否满,满则 pthread_cond_wait() 释放锁并休眠;放入后 signal 非空条件
buffer_get() while 检查空且未结束;g_stop 用于优雅退出——生产结束后消费者把剩余元素消费完再退出
pthread_cond_signal vs broadcast 每次放入/取出用 signal 唤醒一个等待者;结束时用 broadcast 唤醒所有消费者
g_stop 共享退出标志,同样在锁保护下读写,避免遗漏唤醒导致消费者永久阻塞
主线程 join 顺序 先等生产者全部结束,再设置 g_stop 并广播,最后等消费者退出,保证不丢数据

编译运行:

gcc -Wall -pthread -o pc pc.c
./pc

3. API 速查表

3.1 线程生命周期

函数 一句话作用
pthread_self() 获取当前线程 ID
pthread_equal() 比较两个线程 ID
pthread_create() 创建线程
pthread_exit() 终止调用线程,可带退出码
pthread_join() 阻塞等待线程终止并回收资源
pthread_detach() 分离线程,终止后自动回收
pthread_cancel() 向线程发送取消请求
pthread_testcancel() 手动产生取消点
pthread_setcancelstate() 设置可取消/不可取消
pthread_setcanceltype() 设置延迟/异步取消
pthread_cleanup_push()/pop() 注册/执行线程清理函数

3.2 线程属性与数据

函数 一句话作用
pthread_attr_init()/destroy() 初始化/销毁线程属性对象
pthread_attr_setstacksize() 设置线程栈大小
pthread_attr_setdetachstate() 设置创建时是否分离
pthread_once() 保证初始化代码只执行一次
pthread_key_create()/delete() 创建/删除线程特有数据键
pthread_setspecific()/getspecific() 设置/获取线程私有数据

3.3 同步对象

对象 初始化 加锁/等待 解锁/通知 销毁
互斥锁 pthread_mutex_init / PTHREAD_MUTEX_INITIALIZER pthread_mutex_lock / trylock pthread_mutex_unlock pthread_mutex_destroy
条件变量 pthread_cond_init / PTHREAD_COND_INITIALIZER pthread_cond_wait / timedwait pthread_cond_signal / broadcast pthread_cond_destroy
自旋锁 pthread_spin_init pthread_spin_lock / trylock pthread_spin_unlock pthread_spin_destroy
读写锁 pthread_rwlock_init / PTHREAD_RWLOCK_INITIALIZER pthread_rwlock_rdlock / wrlock pthread_rwlock_unlock pthread_rwlock_destroy

4. 实验步骤与调试方法

4.1 实验步骤(以互斥锁修正竞态为例)

  1. 在 Ubuntu 下建工作目录:

    mkdir -p ~/vscode_ws/12_thread_sync
    cd ~/vscode_ws/12_thread_sync
    
  2. 创建 race.c,粘贴 2.1 节的无锁版本,编译运行:

    gcc -Wall -pthread -o race race.c
    ./race 10000000
    

观察 g_count 通常小于 2000 万。

  1. 创建 mutex.c,粘贴 2.2.2 的加锁版本,用同样的参数运行,观察结果稳定为 2000 万。

  2. 把互斥锁分别替换为自旋锁、读写锁,比较运行耗时(可用 time ./app)。

  3. 编译生产者-消费者程序并运行,观察"放入/取出"配对是否正常、程序能否正常退出。

4.2 调试方法

现象 可能原因 排查手段
链接报 undefined reference to 'pthread_create' 未链接 pthread 库 编译加 -lpthread-pthread
g_count 结果偏小 共享变量未加锁,存在竞态 用互斥锁/自旋锁保护读写
程序卡死不退出 死锁;或条件变量等待无人唤醒 检查加锁顺序;确认有 signal/broadcast;用 while 重查条件
pthread_join 返回 Invalid argument 目标线程已分离 分离的线程不能再 join
取消请求不生效 线程处于不可取消状态,或循环内无取消点 检查 pthread_setcancelstate();加 pthread_testcancel()
清理函数不执行 线程用 return 退出,或 pop 参数为 0 改用 pthread_exit(),或 pthread_cleanup_pop(1)
pthread_cancel 返回非 0 pthread 函数失败返回错误码且不设置 errno strerror(ret) 翻译错误码
多线程程序崩溃 使用了线程不安全函数或共享数据未保护 查 man 的 ATTRIBUTES;确认 MT-Safe

调试常用命令:

gcc -Wall -Wextra -pthread -g -o app app.c   # 打开警告与调试信息
man 7 pthreads                                # 取消点、线程不安全函数列表
man 7 signal                                  # 异步信号安全函数列表
gdb ./app                                     # 调试
# (gdb) info threads / thread apply all bt    # 查看所有线程与调用栈

死锁与数据竞争也可借助 helgrindThreadSanitizergcc -fsanitize=thread)等工具定位。


5. 跨平台对比:IMX6ULL vs STM32 vs RK3568

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

维度 I.MX6ULL(本教程) STM32(裸机 / FreeRTOS) RK3568
内核 Cortex-A7(单核) Cortex-M3/M4/M7 Cortex-A55(多核)
操作系统 完整 Linux 裸机或 FreeRTOS 等 RTOS 完整 Linux
线程模型 POSIX pthread(用户态线程) 裸机无线程;RTOS 为任务(Task) POSIX pthread,同 I.MX6ULL
创建接口 pthread_create() xTaskCreate()(FreeRTOS) pthread_create()
同步机制 mutex / cond / spin / rwlock(POSIX) 信号量、互斥量、事件组(RTOS API) 与 I.MX6ULL 相同
调度 内核 CFS 抢占式调度 优先级抢占式调度(RTOS) 内核 CFS 调度
线程栈 每线程独立栈,可配置大小 每任务独立栈,创建时指定数组 每线程独立栈
内存保护 MMU,用户/内核态隔离 无 MMU(M 核),无进程概念 MMU,用户/内核态隔离
工具链 arm-linux-gnueabihf-gcc -pthread arm-none-eabi-gcc + RTOS 源码 aarch64-linux-gnu-gcc -pthread
迁移要点 pthread 接口 Linux 间通用 需从 RTOS API 改写为 pthread 与 I.MX6ULL 代码几乎一致,注意 64 位与库版本

结论:I.MX6ULL 与 RK3568 同属 Linux 用户态编程,本篇的 pthread API 可直接复用;STM32 裸机没有线程概念,使用 FreeRTOS 时则是另一套任务 API(xTaskCreatexSemaphoreTake 等),概念相似但接口完全不同,切忌混用。


6. 面试精选

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

Q1:线程和进程有什么区别?什么时候用多线程、什么时候用多进程?

  • 调度单位不同:线程是参与系统调度的最小单位,进程只是资源分配的容器;
  • 地址空间不同:同一进程内多个线程共享进程地址空间,进程之间相互独立、隔离;
  • 资源开销不同:线程创建和切换开销远小于进程;线程共享进程的文件描述符、代码段、数据段、堆等,额外只有线程栈、寄存器、TLS 等;
  • 通信方式不同:线程间可直接读写全局变量(需同步),进程间需要 IPC;
  • 健壮性不同:一个线程崩溃可能拖垮整个进程;进程崩溃一般不影响其它进程;
  • 私有部分:线程有各自的调用栈、寄存器环境、线程本地存储。

选择:需要频繁通信、频繁切换、要求高并发且共享数据多时用多线程(如数据处理、GUI);需要高隔离性、高健壮性、避免共享状态时用多进程(如网络服务器为每个请求 fork 子进程)。

Q2:pthread_create 的 start 函数为什么只能接收一个 void * 参数?如何传多个参数?

pthread_create()start_routine 签名固定为 void *(*)(void *),这是 POSIX 的规定,所以只能传递一个 void *。要传多个参数时,把这些参数封装到一个结构体中,把结构体地址作为 arg 传入。

关键约束:arg 指向的对象在线程整个生命周期内必须一直有效,因此应指向全局变量或堆变量,不能指向会失效的栈变量。例如:

struct thread_arg {
    int id;
    char name[32];
    int result;
};

static void *worker(void *arg)
{
    struct thread_arg *p = (struct thread_arg *)arg;
    p->result = p->id * 2;
    return NULL;
}

返回值也是 void *,可返回结构体指针(同样不应指向线程栈),由 pthread_join() 取出。

Q3:pthread_join、pthread_detach、pthread_cancel 分别是什么?有什么区别?

  • pthread_join():阻塞等待目标线程终止并回收资源,可获取退出码。未分离线程必须 join,否则会变成僵尸线程。线程之间关系对等,任意线程都能 join 另一个线程;不能非阻塞调用
  • pthread_detach():把线程设为分离状态,线程终止时系统自动回收资源,之后不能再 join,且不可逆。适合不关心返回值的"即发即忘"线程。
  • pthread_cancel():向线程发送取消请求,立即返回,不等待线程退出。默认行为如同 pthread_exit(PTHREAD_CANCELED)。线程可设置 PTHREAD_CANCEL_DISABLE 不被取消,取消类型分 DEFERRED(到取消点才响应)和 ASYNCHRONOUS(任意时刻)。

区别总结:join 是"等待并回收",detach 是"让系统自动回收",cancel 是"请求对方退出"。

Q4:互斥锁、自旋锁、读写锁、条件变量各适合什么场景?

机制 特点 适用场景
互斥锁 获取失败阻塞休眠,通用 一般临界区保护,使用最广
条件变量 无状态,配合互斥锁,等待条件成立 生产者-消费者、事件通知
自旋锁 获取失败原地自旋,不休眠,临界区必须极短 临界区极短;可用于中断上下文
读写锁 读-读可并发,读-写/写-写互斥 读远多于写的共享数据

补充:互斥锁基于自旋锁实现;自旋锁占用 CPU,长时间自旋会降低效率;读写锁提高了读场景的并行性,但实现更复杂、写者可能饥饿。实际工程中用得最多的是互斥锁 + 条件变量组合。

Q5:什么是线程安全?什么是可重入?两者是什么关系?

  • 线程安全函数:被多个线程同时调用时总能产生正确结果。可通过加锁等同步技术实现。
  • 可重入函数:被同一进程的多个执行流(包括信号处理函数执行流)同时调用时总能产生正确结果。要求更严格,通常要求只使用局部变量、参数和返回值为值类型、调用的其它函数也可重入。
  • 关系:可重入函数一定是线程安全函数;线程安全函数不一定是可重入函数(因为它可能修改外部全局变量,只是用锁保护了)。可重入是线程安全函数的真子集。
  • 再严格一层是异步信号安全函数(可在信号处理函数中安全调用),可重入函数一定是异步信号安全函数。
  • 实践:用 man 查 ATTRIBUTES,MT-Safe 表示线程安全、MT-Unsafe 表示线程不安全;有 _r 后缀的通常可重入。一个安全的信号处理函数只能调用异步信号安全函数(如 write),不能调用 printfmalloc 等。

内容来源:《I.MX6U嵌入式Linux C应用编程指南》第十一、十二章