深入理解线程:从概念到实践(包含线程池实现)

06 线程

什么是线程?

线程是进程中的一条执行流程,它是CPU调度的最小单位。

​ 如图,下面进程中有三个线程,一个主线程,3个子线程,每个线程都有自己的栈。同一进程中的线程执行相同的程序(共享代码段),且共享数据段,堆,内存映射区等内存空间。

所有线程均驻留在同一虚拟地址空间。这就意味着,各线程栈中数据也是共享的。但是要慎重使用这种方式,如果线程终止,新的线程也有可能就会对已经终止的线程空间重新加以利用,从而产生难以捉摸的BUG。

同一进程的多个线程可以并发执行。在多处理操作环境下,可以多线程同时进行并行执行。如果某一线程因IO操作而阻塞,其他进程依旧可以继续运行。

并发与并行

CPU指令是原子的,也就是只能执行成功或者失败。

并发:

在同一时间段,执行多个线程的指令,也就是两个线程的执行时间有重叠部分(everlap)。

并发不需要多个CPU,一个CPU也可是出现并发现象。

并行:

同一时间段,两个线程同时执行。所以需要多个CPU。

所以并发包含并行。并发是一种现象,并行是一种特殊的并发,是一种能力(需要多个核心)。

同步与异步

异步操作:几个操作同时执行,操作由新的线程或进程执行,不阻塞当前线程的执行

同步操作:多个操作一次执行,一个操作未完成,后续操作不启动

同步和异步是描述程序执行流程的两种基本模式,它们主要区别在于任务执行过程中是否阻塞后续代码的执行。

同步

同步指的是程序中的任务按照顺序依次执行。在执行一个任务时,必须等待这个任务完成之后才能开始下一个任务。这意味着,在同步模式下,如果一个任务执行时间较长(例如,网络请求、文件读写等),那么整个程序会被阻塞,直到该任务完成才能继续执行后面的代码。这通常会导致应用程序响应性变差,尤其是在处理I/O操作时。

特点:

  • 任务按顺序执行。
  • 每个任务必须等待前一个任务完成后才能开始。
  • 在执行耗时操作时可能会导致程序暂时失去响应。

异步

异步则允许程序在等待某些操作完成的同时继续执行其他任务。这意味着当发起一个异步调用(比如发送网络请求或读取文件)后,程序不会停下等待该操作完成,而是继续执行后续代码。当异步操作完成时,会通过回调函数、事件或者未来的某种机制通知程序,以便处理结果。这种方式可以显著提高程序的效率和响应速度,特别适合于执行I/O密集型任务。

特点:

  • 允许同时执行多个任务,不需等待每个任务完成即可开始新的任务。
  • 可以提高程序的整体性能和响应速度。
  • 处理复杂时可能增加代码的理解难度和维护成本。

实际应用

  • 同步适用场景:适用于逻辑简单,不需要考虑并发操作的情况;或者是需要确保一系列操作严格按顺序执行的场合。
  • 异步适用场景:对于涉及大量I/O操作的应用(如Web服务器、数据库访问等),使用异步编程模型可以有效提高资源利用率和系统吞吐量。

为什么引入线程?

多进程应用的局限性

  1. 进程隔离导致通信开销大
    • 进程间存在隔离屏障,进程间通信需要打破隔离,开销较大
  2. 进程创建成本高
    • 调用fork()创建进程代价较高,即使使用写时复制(copy-on-write)技术,仍需复制页表(page table)、文件描述符列表等多种属性,整体开销依然较大。
  3. 进程切换性能影响
    • 进程间切换容易导致CPU高速缓存(cache)失效,可能导致页表缓存(TLB)失效。

线程解决方案的优势

  1. 高效的资源共享
    • 线程间可方便快速地共享信息,只需将数据放入共享变量即可。注意:需要同步技术避免多线程同时修改同一数据
  2. 创建效率高
    • 创建线程比创建进程快10倍以上
  3. 切换性能优化
    • 同一进程的线程共享虚拟内存空间,线程切换不易导致CPU高速缓存失效,对页表缓存(TLB)影响较小。

同一进程中的线程会共享 进程的代码段,数据段(全局变量,静态变量等) ,堆空间,文件,信号以及进程上下文等内容,但是为了的独立运行也有独享的资源:

  • 线程栈 :每个线程有独立栈空间,保存局部变量、函数调用栈帧。像线程里的局部变量 int local_var,其他线程无法直接访问,避免了调用栈混乱。
  • 程序计数器(PC)与寄存器:线程切换时,PC (记录下一条要执行的指令地址 )、寄存器(存储临时数据 )会保存 / 恢复,保证线程执行流程独立。
  • 线程 ID、优先级:每个线程有唯一 ID 供进程识别,优先级也可独立设置,调度器依此决定线程执行机会。

线程的生命周期

创建

就绪

​ 线程活着但是没有获得处理器时间。

运行

​ CPU分配的时间片耗尽之后,线程回到就绪状态,等待下一次分配时间片,再次进入运行状态

阻塞

结束

C语言 中,线程的状态管理主要依赖于 POSIX 线程库(pthread)。以下是线程的生命周期及其状态转换的详细说明,结合 C 语言的实现方式:


1. 线程的状态

(1) 创建状态(New)
  • 定义:线程对象被创建,但尚未启动。

  • 触发条件 :调用 pthread_create() 函数创建线程。

  • 特点

    • 线程尚未被操作系统调度。

    • 示例代码:

      c 复制代码
      pthread_t thread;
      pthread_create(&thread, NULL, thread_func, NULL); // 线程处于创建状态
(2) 就绪状态(Runnable)
  • 定义:线程已准备好运行,等待操作系统分配 CPU 时间片。

  • 触发条件pthread_create() 返回后,线程进入就绪队列。

  • 特点

    • 线程可能未立即执行,需等待调度。

    • 示例代码:

      复制代码
      // 创建线程后,线程进入就绪状态
(3) 运行状态(Running)
  • 定义:线程获得 CPU 时间片,正在执行代码。

  • 触发条件:操作系统从就绪队列中选择线程并分配 CPU 资源。

  • 特点

    • 线程执行 thread_func 中的代码。

    • 示例代码:

      c 复制代码
      void* thread_func(void* arg) {
          printf("Thread is running\n");
          return NULL;
      }
(4) 阻塞状态(Blocked/Waiting)
  • 定义:线程因等待资源(如锁、I/O)或主动阻塞而暂停执行。

  • 触发条件

    • 锁竞争 :调用 pthread_mutex_lock() 时无法获取锁。
    • 主动阻塞 :调用 sleep()pthread_cond_wait() 或执行 I/O 操作。
  • 特点

    • 阻塞的线程不会消耗 CPU 资源。

    • 示例代码:

      c 复制代码
      pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
      
      void* thread_func(void* arg) {
          pthread_mutex_lock(&mutex); // 如果锁被占用,线程进入阻塞状态
          // 访问共享资源
          pthread_mutex_unlock(&mutex);
          return NULL;
      }
(5) 结束状态(Terminated)
  • 定义:线程运行完成或因异常终止,生命周期结束。

  • 触发条件

    • 正常结束 :线程函数 thread_func 返回或调用 pthread_exit()
    • 异常结束:线程因未处理的错误退出。
  • 特点

    • 线程资源需通过 pthread_join() 显式回收。

    • 示例代码:

      c 复制代码
      void* thread_func(void* arg) {
          printf("Thread is terminating\n");
          pthread_exit(NULL); // 线程主动结束
      }

2. 状态转换图

复制代码
[New] → [Runnable] ↔ [Running] ↔ [Runnable]
          ↓              ↑
       [Blocked  /   Waiting]  
          ↓
    [Terminated]

3. 状态转换的关键函数

状态转换 触发函数/操作 说明
创建 → 就绪 pthread_create() 创建线程后进入就绪状态。
就绪 ↔ 运行 操作系统调度(无需显式调用) 系统根据调度策略分配 CPU 时间片。
运行 → 阻塞 pthread_mutex_lock()sleep() 线程因等待锁或主动阻塞进入阻塞状态。
阻塞 → 就绪 pthread_mutex_unlock()、超时、唤醒 资源可用或超时后,线程重新进入就绪状态。
运行 → 结束 returnpthread_exit()、异常退出 线程函数执行完毕或主动退出。

4. 完整示例代码

c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* thread_func(void* arg) {
    printf("Thread is running\n");

    pthread_mutex_lock(&mutex); // 进入阻塞状态(如果锁被占用)
    printf("Thread acquired the lock\n");
    sleep(2); // 主动阻塞 2 秒
    pthread_mutex_unlock(&mutex);

    printf("Thread is terminating\n");
    pthread_exit(NULL); // 主动结束线程
}

int main() {
    pthread_t thread;
    pthread_create(&thread, NULL, thread_func, NULL); // 创建线程

    // 等待线程结束并回收资源
    pthread_join(thread, NULL);

    printf("Main thread finished\n");
    return 0;
}

5. 注意事项

  1. 线程终止
    • 不推荐使用 pthread_cancel() 强制终止线程(可能导致资源泄漏)。
    • 推荐通过共享变量或标志位协作式结束线程。
  2. 资源回收
    • 使用 pthread_join() 确保线程资源被正确回收。
    • 忽略 pthread_join() 可能导致僵尸线程。
  3. 阻塞操作
    • 避免在持有锁时执行阻塞操作(如 sleep()),可能导致死锁。
  4. 线程安全
    • 使用互斥锁(pthread_mutex_t)保护共享资源,避免竞态条件。

6. 总结

C 语言中线程的状态转换完全依赖于 pthread 库操作系统调度 。开发者需通过调用 pthread_create()pthread_mutex_lock()pthread_exit() 等函数显式管理线程的生命周期,并遵循线程安全的最佳实践。

线程的基本操作

pthread_self() 获取线程标识

类比getpid()

c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>

void print_ids(const char* whichphread)
{
    //这里默认都可以成成功返回
    printf("pid = %d, ppid = %d, tid = %lu\n",
            getpid(),
            getppid(),
            pthread_self());
}

//注意手册中有说到,在编译时要加参数 -pthread 链接pthread库文件
int main(int argc, char* argv[])
{
    print_ids("main pthread");
    return 0;
}

pthread_creat() 创建线程

类比fork()

程序启动时只有一个线程,称为主线程初始线程 。使用 pthread_create() 函数可以创建新线程。(每个线程就是一个执行流程。)

c 复制代码
#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
    void* (*start_routine)(void *), void *arg);
//void* 可以指向任意类型

// 成功:返回 0
// 失败:返回错误状态码(不会设置 errno)
  • pthread_create() 调用成功后,应用程序无法确定接下来调度哪一个线程
  • 在多处理器系统中,多个线程可能会在不同 CPU 上同时执行(并行)
参数 类型 说明
thread pthread_t * 传出参数,用来存放新线程 ID
attr const pthread_attr_t * 传入参数,指定新线程属性。 通常传 NULL 表示采用默认属性
start_routine void* (*)(void *) 函数指针,指向线程入口函数。 新线程从该函数开始执行,函数返回时线程结束
arg void * 传递给 start_routine 的实参
c 复制代码
void* start_routine(void* arg) {
    // 新线程的执行流程
}

特性

  • 可以接收任意类型 (void*) 的参数
  • 可以返回任意类型 (void*) 的值
  • 其他线程可以通过 pthread_join() 获取该返回值
c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <error.h>

void print_ids(const char* whichphread)
{
    //这里默认都可以成成功返回
    printf("%s : pid = %d, ppid = %d, tid = %lu\n",
            whichphread,
            getpid(),
            getppid(),
            pthread_self());
}

void* start_routine(void * argc)
{
    print_ids("new pthread:");
    //因为要返回值 所以返回NULL
    return NULL;
}

//注意手册中有说到,在编译时要加参数 -pthread 链接pthread库文件
int main(int argc, char* argv[])
{
    print_ids("main pthread");

    //创建新线程
    //新线程的id
    pthread_t newthread;
    //phread_create() 函数的返回值 ,成功返回 创建的线程的线程标号id 失败返回 错误状态码 并且会不设置errno
    //函数的参数 1 传入传出参数 会在函数中改变 返回新进程的id  参数2 指定新线程的属性 一般传NULL,表示使用默认属性
    //参数3 函数指针,指向线程的入口函数,新线程从入口开始执行,等到函数结束或者返回该线程结束
    //参数4 给start_routine的参数
    int err = pthread_create(&newthread, NULL, start_routine, NULL);
    printf("main thread : create a new thread %lu\n", newthread);
    //返回值不等于0 线程创建错误
    if(err)
    {
        error(1, err, "pthread_create");
    }
    sleep(3);
    return 0;
}

创建的新线程,从函数参数中的函数入口开始执行,到达return 0 或者 函数执行到结尾时,该线程就会正常终止。

为什么会出现这种情况?

进程的结束标志着该进程内线程的死亡。

c 复制代码
//该程序主要演示 pthread_creat()最后一个参数的传参的使用

#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <error.h>

struct foo
{
    int a;
    int b;
    int c;
};

void print_ids(const char* whichphread)
{
    //这里默认都可以成成功返回
    printf("%s : pid = %d, ppid = %d, tid = %lu\n",
            whichphread,
            getpid(),
            getppid(),
            pthread_self());
}

void* start_routine(void * argc)
{
    print_ids("new pthread:");

    struct foo* a = (struct foo*)argc;

    printf("new thread : a = {%d, %d, %d}\n", a->a, a->b , a->c);	//a指向main线程的栈空间
    
    //因为要返回值 所以返回NULL
    return NULL;
}

//注意手册中有说到,在编译时要加参数 -pthread 链接pthread库文件
int main(int argc, char* argv[])
{
    print_ids("main pthread");

    //结构体可以直接赋值
    struct foo a = {1, 2, 3};

    //创建新线程
    //新线程的id
    pthread_t newthread;
    //phread_create() 函数的返回值 ,成功返回 创建的线程的线程标号id 失败返回 错误状态码 并且会不设置errno
    //函数的参数 1 传入传出参数 会在函数中改变 返回新进程的id  参数2 指定新线程的属性 一般传NULL,表示使用默认属性
    //参数3 函数指针,指向线程的入口函数,新线程从入口开始执行,等到函数结束或者返回该线程结束
    //参数4 给start_routine的参数
    
    int err = pthread_create(&newthread, NULL, start_routine, &a);
    printf("main thread : create a new thread %lu\n", newthread);
    //返回值不等于0 线程创建错误
    if(err)
    {
        error(1, err, "pthread_create");
    }
    sleep(3);
    return 0;
}

上面示例要注意,参数的生命,主线程中创建的变量,传递的参数给新的进程,要注意主进程的结束要后于新进程,否则会出现参数指针的悬空。

void*

void*可以理解为通用类型指针,具备指向任意类型数据的能力,但有特殊规则

核心特性:可以指向任意类型,但需转化后使用

void* 是一种无类型指针,关键特点:

  • 可以指向int , float, char , 结构体,数组等任意数据类型的地址,如
c 复制代码
int a = 10;
float b = 3.14;
void* p;
p = &a; // 指向 int 类型
p = &b; // 指向 float 类型,合法
  • 但是无法解引用(*p),或者进行算数运算(p++),因为编译器不知道他指向的数据的具体的数据类型,占多少个字节
  • 简单来说,void* 是 "容器",能装任意类型的指针,但用的时候得 "拆箱"(转换为具体类型)。

pthread_exit() 线程终止

线程的终止方式有以下几种:

  • 进程终止 (比如调用 exit(),从 main() 函数返回),那么该进程的所有线程都会立即终止。
  • 从线程的入口函数返回。
  • 线程调用 pthread_exit()。可以类比进程的_exit()
  • 响应 pthread_cancel() 的取消请求。响应信号。可以类比进程终止的信号终止,也就是异常终止。

pthread_exit() 函数将终止调用线程。

c 复制代码
#include <pthread.h>

void pthread_exit(void *retval);
// 总是成功

主线程起一个统筹调度的作用,主线程接收用户的请求,然后主线程会分发给子线程进行操作,然后通过接收子线程的返回值,进行下一步操作。然后子线程会将结果返回主线程。

参数讲解

retval: 线程的返回值。返回任意类型的值,其它线程(一般为主线程)可以通过 pthread_join() 获取该返回值。

NOTE

线程的返回值不应位于该线程的栈中!因为线程终止后,系统可能会将这片内存区域分配给一个新的线程使用。

如果主线程调用了 pthread_exit(),而不是调用 exit() 或是执行 return 语句,那么其他线程将继续运行。

当你注释掉 return 0; 并使用 pthread_exit(NULL); 时,主进程(主线程)会正常退出,但不会立即终止整个进程。这意味着其他线程(如你创建的新线程)仍然有机会执行它们的任务。

然而,如果你不注释掉 return 0;,那么当主线程执行到 return 0; 时,它会调用 exit(0) 来终止整个进程。exit(0) 不仅会终止主线程,还会终止所有其他线程,包括你新创建的线程。因此,在这种情况下,新线程可能没有足够的时间来开始执行或完成它的任务就被强制终止了。

当一个进程中所有的线程都终止了,包括主线程在内的所有工作线程结束后,该进程就会自动终止。在多线程编程中,主线程通常负责创建其他的工作线程,并且这些线程可以并行执行任务。一旦所有的线程完成了它们的任务并且退出(通过从线程函数返回或调用pthread_exit()),如果没有其他线程存在,进程将结束。

pthread_join() 连接已终止的线程

一般为了接收线程的返回值。可以类比waitpid(), waitppid()

pthread_join() 会等待线程 ID 为 thread 的线程终止(如果该线程已经终止,pthread_join() 会立刻返回)。这个操作叫做连接。

c 复制代码
#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);
// 成功: 0
// 失败: 返回错误状态码,不会设置errno

参数讲解

retval: 传出参数,用来接收 ID 为 thread 的线程的返回值。

因为线程可以返回任意值 (void*),所以这个传出参数的类型为 (void**)。

具体来说:

  • 线程可以通过 pthread_exit() 或者从线程入口函数返回一个 void* 类型的退出状态。
  • 调用 pthread_join() 的函数需要知道如何接收这个退出状态。为此,它会提供一个 void* 类型的变量,并将其地址(即 void**)传递给 pthread_join()
  • pthread_join() 函数然后将线程的退出状态写入由 void** 参数指向的位置。
c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <error.h>
#include <errno.h>

typedef struct{
    int a;
    int b;
    int c;
}Foo;

void* start_routine(void* args)
{
    Foo x = {1, 2, 3};
    return &x;
}

int main()
{
    //创建线程
    pthread_t tid;
    int err = pthread_create(&tid, NULL, start_routine, NULL);
    if(err)
    {
        error(1, err, "pthread_create");
    }
    Foo* x;
    pthread_join(tid, (void**)&x); //阻塞,等待tid线程结束
    printf("mian pthread : retval = {%d, %d, %d}", x->a, x->b, x->c);

    return 0;
}

会出现段错误,因为返回的是线程栈上的数据,函数结束,栈空间会清空。

将空间建在堆上再返回

c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <error.h>
#include <errno.h>
#include <stdlib.h>

typedef struct{
    int a;
    int b;
    int c;
}Foo;

void* start_routine(void* args)
{
    Foo* x = (Foo*)malloc(sizeof(Foo));
    x->a = 1;
    x->b = 2;
    x->c = 3;
    
    return x;
}

int main()
{
    //创建线程
    pthread_t tid;
    int err = pthread_create(&tid, NULL, start_routine, NULL);
    if(err)
    {
        error(1, err, "pthread_create");
    }
    Foo* x;
    pthread_join(tid, (void**)&x); //阻塞,等待tid线程结束
    printf("mian pthread : retval = {%d, %d, %d}\n", x->a, x->b, x->c);

    free(x);
    return 0;
}

pthread_detach() 线程分离

默认情况下,线程是可连接的 (joinable),也就是说,当线程退出时,其他线程可以通过 pthread_join() 获取它的返回状态。有时,我们并不关心线程的返回状态,反而希望在线程终止时系统能够自动清理并移除它。在这种情况下,我们可以调用pthread_detach() 将线程标记为处于分离 (detached) 状态。

c 复制代码
#include <pthread.h>

int pthread_detach(pthread_t thread);
// 成功: 0
// 失败: 返回错误状态码,不会设置errno

我们可以调用下面的代码,让线程可以自行分离:

c 复制代码
pthread_detach(pthread_self());
c 复制代码
//测试 pthread_detach函数

#include <stdio.h>
#include <pthread.h>
#include <error.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>

void* start_routine(void* args)
{
    sleep(3);
    int err = pthread_detach(pthread_self());
    if(err)
    {
        fprintf(stderr, "pthread_detch is fail\n");
    }
    return (int*)1111;
}
int main()
{
    pthread_t tid;
    int err = pthread_create(&tid, NULL, start_routine, NULL);
    if(err)
    {
        error(1, err, "pthread_create");
    }
    int ret; 
    err = pthread_join(tid, (void*)&ret);
    if(err)
    {
        error(1, err, "pthread_join");
    }
    printf("main pthread: 0x%lx termianted\n", tid);
    printf("retval : %d\n", ret);
    return 0;
}

一旦线程处于分离状态,就不能再使用 pthread_join() 来获取其返回状态了,也无法再回到可连接 (joinable) 状态了。

在子线程调用 pthread_detach(pthread_self()); 之前,如果主线程已经成功调用了 pthread_join(),则不会有任何问题,主线程能正常获取子线程的退出状态。但是,这种场景下,子线程后续再尝试自我分离是没有意义的,因为此时子线程已经结束了。

c 复制代码
//测试 pthread_detach函数

#include <stdio.h>
#include <pthread.h>
#include <error.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>

void* start_routine(void* args)
{
    // sleep(3);
    return (int*)1111;
}
int main()
{
    pthread_t tid;
    int err = pthread_create(&tid, NULL, start_routine, NULL);
    if(err)
    {
        error(1, err, "pthread_create");
    }
    err = pthread_detach(tid);
    if(err)
    {
        fprintf(stderr, "pthread_detch is fail\n");
    }
    int ret; 
    err = pthread_join(tid, (void*)&ret);
    if(err)
    {
        error(1, err, "pthread_join");
    }
    printf("main pthread: 0x%lx termianted\n", tid);
    printf("retval : %d\n", ret);
    return 0;
}

分离之后,不可连接。

pthread_cancel 取消线程

通常情况下,程序中的多个线程会并发执行,每个线程各司其职。但有时候,需要取消一个线程的执行。比如:

  • 一组线程共同在执行一个计算,一旦某个线程检测到错误,就可以通知其它线程终止执行了。
  • 一个由图形用户界面 (GUI) 驱动的应用程序可能会提供一个"取消"按钮,以便用户可以随时终止后台某一线程正在执行的任务。这种情况下,控制图形用户界面的线程(一般是主线程)就需要请求后台执行任务的线程退出。

pthread_cancel() 函数会向线程 ID 为 thread 的线程发送一个取消请求。

c 复制代码
#include <pthread.h>

int pthread_cancel(pthread_t thread);
// 成功: 0
// 失败: 返回错误状态码,不会设置errno

参数讲解

thread: 目标线程的 ID

发送取消请求后,pthread_cancel() 函数会立即返回,不会等待目标线程退出。目标线程会不会响应取消请求?以及何时响应?这取决于目标线程的两个属性:取消状态 (cancellation state) 和取消类型 (cancellation type)。

取消状态

目标线程会不会响应取消请求,是由目标线程的取消状态决定的,它有两个取值:

  • PTHREAD_CANCEL_ENABLE: 线程可以取消。这是默认值。
  • PTHREAD_CANCEL_DISABLE: 线程不可取消。如果此类线程收到取消请求,则会将取消请求挂起,直到线程的取消状态设置为启用。

pthread_setcancelstate() 可以设置线程的取消状态。

c 复制代码
#include <pthread.h>

int pthread_setcancelstate(int state, int *oldstate);
// 成功: 0
// 失败: 返回错误状态码,不会设置errno

参数讲解

state: 取消状态,可取下面两个值:

  • PTHREAD_CANCEL_ENABLE: 线程可以取消。这是默认值。
  • PTHREAD_CANCEL_DISABLE: 线程不可取消。如果此类线程收到取消请求,则会将取消请求挂起,直到线程的取消状态设置为启用。

oldstate: 传出参数,用来保存以前的取消状态。

取消类型

目标线程何时响应取消请求,是由目标线程的取消类型决定的,它也有两个取值:

  • PTHREAD_CANCEL_DEFERRED: 延迟响应,挂起取消请求,直到下一个取消点才响应。这是默认值。
  • PTHREAD_CANCEL_ASYNCHRONOUS: 异步响应,可以在任意时间点响应取消请求(未必是立即响应)。异步响应在实际应用中很少使用。

pthread_setcanceltype() 可以设置线程的取消类型。

c 复制代码
#include <pthread.h>

int pthread_setcanceltype(int type, int *oldtype);
// 成功: 0
// 失败: 返回错误状态码,不会设置errno

参数讲解

type: 取消类型,可取下面两个值:

  • PTHREAD_CANCEL_DEFERRED: 延迟响应,挂起取消请求,直到下一个取消点才响应。这是默认值。
  • PTHREAD_CANCEL_ASYNCHRONOUS: 异步响应,可以在任意时间点响应取消请求(未必是立即响应)。异步响应在实际应用中很少使用。

oldtype: 传出参数,用来保存以前的取消类型。

取消点

若将线程的取消性状态和类型分别置为 ENABLE 和 DEFERRED(延期的),那么只有当线程执行到某个取消点 (cancellation point) 时,取消请求才会起生效。取消点是一组函数,这些函数往往可以让线程陷入无限期的阻塞。

SUSv3 标准规定下面这些函数必须是取消点。

accept() nanosleep() sem_timedwait()
aio_suspend() open() sem_wait()
clock_nanosleep() pause() send()
close() poll() sendmsg()
connect() pread() sendto()
creat() pselect() sigpause()
fcntl(F_SETLKW) pthread_cond_timedwait() sigsuspend()
fsync() pthread_cond_wait() sigtimedwait()
fdatasync() pthread_join() sigwait()
getmsg() pthread_testcancel() sigwaitinfo()
getpmsg() putmsg() sleep()
lockf(F_LOCK) putpmsg() system()
mq_receive() pwrite() tcdrain()
mq_send() read() usleep()
mq_timedreceive() readv() wait()
mq_timedsend() recv() waitid()
msgrcv() recvfrom() waitpid()
msgsnd() recvmsg() write()
msync() select() writev()

如果线程的取消类型为 DEFERRED,当线程收到取消请求后,它会在下次抵达取消点时终止。如果该线程尚未分离,其它线程调用 pthread_join() 进行连接,会收到一个特殊的返回值 PTHREAD_CANCELED。

默认情况下,线程可以被取消(PTHREAD_CANCEL_ENABLE),但只会在到达下一个取消点时才响应取消请求(PTHREAD_CANCEL_DEFERRED)。这样的设置允许程序在安全的地方处理取消请求,从而有助于维护程序状态的一致性和资源的正确释放。

c 复制代码
//测试 pthread_cancle函数

#include <stdio.h>
#include <pthread.h>
#include <error.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>

void * start_routine(void* args)
{
    //线程的默认取消状态和取消类型为 enable 和 deferred(延期的)就是会在下一个取消点时响应
    int oldstatus;
    pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstatus);
    
    sleep(10);
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &oldstatus);
    for(;;)
    {
        sleep(1);
    }
    return NULL;
}

int main()
{
    //创建新线程
    pthread_t tid;
    int err = pthread_create(&tid, NULL, start_routine, NULL);
    if(err)
    {
        fprintf(stderr, "pthread_create failed");
        pthread_exit(NULL);
    }

    //让子进程先跑
    sleep(3);

    err = pthread_cancel(tid);  //发送取消请求
    if(err)
    {
        fprintf(stderr, "pthread_cancle failed");
        pthread_exit(NULL);
    }
    //使用pthread_join 返回值查看是否响应请求。唯一的方式
    void* retval;
    pthread_join(tid, (void**)&retval);
    if(retval == PTHREAD_CANCELED)
    {
        printf("0x%lx cancled\n", tid);
    }
    return 0;
}

//  测试 PTHREAD_CANCEL_ASYNCHRONOUS
//测试 pthread_cancle函数

#include <stdio.h>
#include <pthread.h>
#include <error.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>

void * start_routine(void* args)
{
    //线程的默认取消类型 取消状态 为 enable 和 deferred(延期的)就是会在下一个取消点时响应
    int oldstatus;
    // pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstatus);
    
    // sleep(10);
    // pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &oldstatus);

    sleep(3);
    //异步不需要等待下一个取消点接到信号立即取消
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &oldstatus);

    // for(;;)
    // {
    //     sleep(1);
    // }
    return NULL;
}

int main()
{
    //创建新线程
    pthread_t tid;
    int err = pthread_create(&tid, NULL, start_routine, NULL);
    if(err)
    {
        fprintf(stderr, "pthread_create failed");
        pthread_exit(NULL);
    }

    //让子进程先跑
    // sleep(3);

    err = pthread_cancel(tid);  //发送取消请求
    if(err)
    {
        fprintf(stderr, "pthread_cancle failed");
        pthread_exit(NULL);
    }
    //使用pthread_join 返回值查看是否响应请求。唯一的方式
    void* retval;
    pthread_join(tid, (void**)&retval);
    if(retval == PTHREAD_CANCELED)
    {
        printf("0x%lx cancled\n", tid);
    }
    return 0;
}

线程清理函数

线程收到取消请求后,会执行到下一个取消点终止。如果线程只是草草地直接终止,可能会让程序处于不一致的状态,比如:对共享变量的修改只进行了一半啦,没有释放互斥锁啦......这样的错误轻则导致其他线程产生错误的结果、发生死锁,重则让进程直接崩溃。

为了规避这一问题,线程可以设置一个或多个清理函数。当线程响应取消请求时,会自动执行这些清理函数。

每个线程都拥有一个清理函数栈。当线程响应取消请求时,会依次执行栈中的清理函数(从栈顶到栈底)。当执行完所有的清理函数后,线程终止。

pthread_cleanup_push() 向调用线程的清理函数栈中添加一个清理函数。

c 复制代码
#include <pthread.h>

void pthread_cleanup_push(void (*routine)(void *), void *arg);

pthread_cleanup_push() 会将函数指针 routine 添加到清理函数栈的栈顶。参数 routine 指向的函数原型如下:

c 复制代码
void routine(void* arg)
{
    /* Code to perform cleanup */
}

调用线程清理函数时,会将 arg 传递给对应的线程清理函数。

pthread_cleanup_pop() 从调用线程的清理函数栈的栈顶删除一个清理函数。

c 复制代码
#include <pthread.h>

void pthread_cleanup_pop(int execute);

如果 execute 不为 0,那么执行出栈的清理函数(不终止线程)。对于不想终止线程,又希望执行清理动作的情况,这非常方便。

Aside

尽管我们把 pthread_cleanup_push()pthread_cleanup_pop() 称为函数,但包括 Linux 在内的很多系统都是通过宏来实现的。这意味着,pthread_cleanup_push() 和与其配对的 pthread_cleanup_pop() 必须一一对应,且应位于同一作用域。

c 复制代码
# define pthread_cleanup_push(routine, arg) \
do { \
 __pthread_cleanup_class __clframe (routine, arg)

/* Remove a cleanup handler installed by the matching pthread_cleanup_push.
If EXECUTE is non-zero, the handler function is called. */
# define pthread_cleanup_pop(execute) \
 __clframe.__setdoit (execute); \
} while (0)

进程退出处理函数执行时机

触发方式 是否执行清理函数 说明
main() 函数正常返回 ✅ 是 程序正常终止,会执行注册的清理函数
调用 exit() 函数 ✅ 是 主动调用退出函数,会执行所有注册的清理函数
收到信号(如 SIGINT、SIGTERM) ❌ 否 如果是通过信号终止(未被捕获处理),不会执行清理函数
子进程调用 _exit()_Exit() ❌ 否 不会触发清理函数,用于快速退出

线程退出处理函数执行时机

触发方式 是否执行清理函数 说明
调用 pthread_exit() ✅ 是 推荐方式,保证清理函数被执行
响应取消请求(pthread_cancel() ✅ 是 自动调用清理函数
线程入口函数中 return ❌ 否 不会自动执行 pthread_cleanup_push 注册的函数
使用 pthread_cleanup_pop(1) ✅ 是(手动) 可以控制是否执行当前栈顶的清理函数

📌 小贴士

  • pthread_cleanup_push()pthread_cleanup_pop() 必须成对出现,且不能跨函数。
  • 清理函数的执行顺序是 先进后出(FIFO),因为是栈式结构(每次 push 放到顶部)。
c 复制代码
// 测试线程退出处理函数 pthread_cleanup_push 与 pthread_cleanup_pop

#include <stdio.h>
#include <pthread.h>
#include <error.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>

void routine(void* args)
{
    char* msg = (char*) args;
    printf("%s\n", msg);
}

//测试 线程清理函数的执行时机
// 1 线程入口函数 返回
// 2 接收取消信号
// 3 调用pthread_exit()线程终止函数

void* start_routine(void* args)
{
    //注册线程清理函数
    //void (routine)(void*)
    pthread_cleanup_push(routine, "111");
    pthread_cleanup_push(routine, "222");
    pthread_cleanup_push(routine, "333");

    pthread_cleanup_pop(1); //不会终止线程

    //线程的执行逻辑
    printf("new thread start!\n");
    sleep(3);   //主线程发送取消请求,这是子线程在sleep在中断点直接响应,并且执行清理函数
    printf("new thread stop!\n");

    //参数为 结束是的返回值,可以通过join接收
    // pthread_exit(NULL);
    return NULL;
    //参数execute 不为0,不会终止线程,就可以执行清理函数又不会终止线程
    pthread_cleanup_pop(0);     //上面返回return 之后永远不会执行,但是要写,因为push pop是成对出现的
    pthread_cleanup_pop(0);
}

int main()
{
    //创建新线程
    pthread_t tid;
    int err = pthread_create(&tid, NULL, start_routine, NULL);
    if(err)
    {
        fprintf(stderr, "pthread_create failed");
        pthread_exit(NULL);
    }
    //让子线程有机会运行,防止主线程结束太快导致子线程没有就会进入运行,进程就结束
    sleep(1);
    // pthread_cancel(tid);  //发送取消请求
    
    // pthread_join(tid, NULL);
    // return 0;
}

如果不加sleep(1)?

❓问题:为什么看不到子线程的输出?

✅ 根本原因:主线程退出太快,整个进程就结束了,子线程还没来得及运行或没执行完就被强制终止。

不加return 0;进程中如果有线程正在运行,不会结束等线程运行完成才会结束。

测试结果:

子线程return 正常结束

pthread_exit()

接受取消信号

线程同步

线程之间可以方便,快速的共享数据,但是如果多个线程同时修改同一数据,就可能引发一些问题,这些问题我们统称为并发安全问题

c 复制代码
#include <...>

void* start_routine(void* arg) {
    long long* value = (long long*)arg;
    for (int i = 0; i < 10000000; i++) {
        (*value)++;
    }
    return NULL;
}

int main(void) {
    long long* value = (long long*)calloc(1, sizeof(long long));

    // 创建两个线程
    // 第一个线程执行 (*value)++ 10000000次
    // 第二个线程也执行(*value)++ 10000000次
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, start_routine, (void*)value);
    pthread_create(&tid2, NULL, start_routine, (void*)value);

    // 主线程等待两个子线程结束,并打印 *value 的值。
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    printf("*value = %lld\n", *value);
    return 0;
}

运行上面的程序,你会发现每次运行的结果都可能不一样(取决于实际调度的情况)。如果程序的结果(或状态),取决于实际的调度情况,那么这种现象我们称之为竞态条件 (race condition)。

竞态条件出现的前提

  1. 多线程
  2. 共享资源(数据,文件,锁 ...)
  3. 操作共享资源(写 ...)

value++ 不是原子性操作(CPU指令级别),怎么理解呢?

value++ 对应多条指令

LOAD &value,reg

ADD 1,reg

WRITE reg, &value

如何在不引入太多额外开销的情况下,避免出现竞态条件?这是并发编程的难点,也是并发编程的核心问题。

为了避免出现竞态条件,我们必须引入一些同步手段。所谓同步,简而言之,就是让某些调度不可能发生(因此,也会有一些性能开销)。

本节介绍线程用来同步的两个工具:互斥量 (mutex,有时又称为互斥锁) 和条件变量 (condition variable)。

  • 互斥量是用来保障线程之间可以互斥地访问共享资源。
  • 条件变量是用来让线程(停止执行)等待某个条件成立,它是一种等待唤醒机制。

线程一:m条指令

线程二:n条指令

有多少种调度?(相对顺序)

Cm+n m

这调度中有好的调度:得到正确的结果,坏的调度会得到错误的结果。

同步:让坏的调度不发生

异步:所有的调度情况都可能发生

同步基本问题

  • 互斥地访问资源 <-> 互斥锁

    • 临界区(对共享资源的访问),切换到临界区要上锁,出来之后要释放锁
  • 等待某个条件成立(等待唤醒机制)

互斥锁

互斥量 (mutex) 有两种状态:已锁定 (locked) 和未锁定 (unlocked)。任何时候,最多只能有一个线程可以锁定互斥量。

本质就是一个有限状态机,

初始化

在线程库 pthread 中,用 pthread_mutex_t 类型表示互斥量。互斥量在使用之前必须进行初始化。

c 复制代码
//全局的
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; // 静态初始化,仅适用于静态变量

//局部的
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* attr); // 动态初始化
// 因为是有限状态机,所以这里传递指针就是为了改变状态。所以都是传出参数。
// 成功: 0
// 失败: 返回错误状态码,不会设置errno
上锁

初始化之后,互斥锁处于未锁定状态。下列函数可以锁定互斥量 mutex

c 复制代码
#include <pthread.h>
#include <time.h>

int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_trylock(pthread_mutex_t* mutex);
int pthread_mutex_timedlock(pthread_mutex_t* mutex, const struct timespec* abstime);
// 成功: 0
// 失败: 返回错误状态码,不会设置errno

如果互斥量当前处于未锁定状态,这些函数会锁定互斥量并立即返回。如果其他线程已经锁定了这一互斥量,那么:

  • pthread_mutex_lock 会一直阻塞,直到该互斥量被解锁。
  • pthread_mutex_trylock() 会立即失败,并返回错误码 EBUSY
  • pthread_mutex_timedlock 可以指定超时时间 abstime,如果在超时时间内,调用线程没能获得互斥量的所有权,那么函数 pthread_mutex_timedlock() 会返回错误码 ETIMEDOUT

Tips

函数 pthread_mutex_trylock()pthread_mutex_timedlock()pthread_mutex_lock() 的使用频率要低很多。在经过良好设计的应用程序中,线程对互斥量的持有时间应尽可能地短,以避免妨碍其他线程的并发执行。

释放锁

函数 pthread_mutex_unlock() 可以将一个互斥量解锁。

c 复制代码
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 成功: 0
// 失败: 返回错误状态码,不会设置errno
销毁

将未锁定状态,切换为未初始化状态。

当不再需要自动或动态分配的互斥量时,应使用 pthread_mutex_destroy() 将其销毁。对于使用 PTHREAD_MUTEX_INITIALIZER 静态初始化的互斥量,则无需调用 pthread_mutex_destroy() 进行销毁。

复制代码
#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 成功: 0
// 失败: 返回错误状态码,不会设置errno

只有当互斥量处于未锁定状态,且后续不再使用它时,才应将其销毁。

pthread_mutex_destroy() 销毁的互斥量,可调用 pthread_mutex_init() 对其重新初始化。

将上面的演示程序加上互斥锁,变为同步

c 复制代码
// 不同步 不使用锁

//后改为 加锁的 同步

#include <stdio.h>
#include <pthread.h>
#include <error.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>

//静态初始化
pthread_mutex_t  mtx = PTHREAD_MUTEX_INITIALIZER;

void* start_routine(void* arg) {

    long long* value = (long long*)arg;
    for (int i = 0; i < 10000000; i++) {
        //加锁
        pthread_mutex_lock(&mtx);
        (*value)++;
        //释放锁
        pthread_mutex_unlock(&mtx);
    }
    return NULL;
}

int main(void) {
    long long* value = (long long*)calloc(1, sizeof(long long));

    // 创建两个线程
    // 第一个线程执行 (*value)++ 10000000次
    // 第二个线程也执行(*value)++ 10000000次
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, start_routine, (void*)value);
    pthread_create(&tid2, NULL, start_routine, (void*)value);

    // 主线程等待两个子线程结束,并打印 *value 的值。
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    printf("*value = %lld\n", *value);
    return 0;
}
银行的例子
c 复制代码
// 银行同步

#include <stdio.h>
#include <pthread.h>
#include <error.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>

typedef struct {
    int id;
    int banlance;
    //注意点2:选择合适粒度的锁,每个账户都使用一把锁
    pthread_mutex_t mut;
} Account;

Account acct1 = {1, 100, PTHREAD_MUTEX_INITIALIZER};

int withdraw(Account* a, int money)
{
    //临界前上锁
    int ret;
    pthread_mutex_lock(&a->mut);
    if(a->banlance <= 0)
    {
        fprintf(stderr, "Error: not enough money!\n");
        pthread_mutex_unlock(&a->mut);
        ret = -1;
        goto end;
    }
    if(a->banlance >= money)
    {
        sleep(1);   //增大线程间切换的概率
        a->banlance -= money;
        ret = money;
        goto end;
    }
    ret = a->banlance;
    a->banlance = 0;
    //注意点1:每次函数的出口都要释放锁
end:
    pthread_mutex_unlock(&a->mut);
    return ret;
}

void* start_routine(void* args)
{
    int money = (int) args;
    int ret = withdraw(&acct1, money);
    if(ret != -1)
    {
        printf("0x%lx: withdraw %d\n", pthread_self(), ret);
    }
    return NULL;
}

int main()
{
    pthread_t tid[2];
    pthread_create(&tid[0], NULL, start_routine, (void*)100);
    pthread_create(&tid[1], NULL, start_routine, (void*)100);

    //直接传线程标号
    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);

    return 0;
}

死锁

c 复制代码
// 银行同步

#include <stdio.h>
#include <pthread.h>
#include <error.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>

typedef struct {
    int id;
    int banlance;
    //注意点2:选择合适粒度的锁,每个账户都使用一把锁
    pthread_mutex_t mut;
} Account;

Account acct1 = {1, 100, PTHREAD_MUTEX_INITIALIZER};
Account acct2 = {2, 1000, PTHREAD_MUTEX_INITIALIZER};

int withdraw(Account* a, Account* b, int money)
{
    //临界前上锁
    int ret;
    pthread_mutex_lock(&a->mut);
    sleep(1);   //增加线程间切换的概率
    pthread_mutex_lock(&b->mut);
    if(a->banlance <= 0)
    {
        fprintf(stderr, "Error: not enough money!\n");
        pthread_mutex_unlock(&a->mut);
        ret = -1;
        goto end;
    }
    if(a->banlance >= money)
    {
        a->banlance -= money;
        b->banlance += money;
        ret = money;
        goto end;
    }
    ret = a->banlance;
    b->banlance += a->banlance;
    a->banlance = 0;
    //注意点1:每次函数的出口都要释放锁
end:
    pthread_mutex_unlock(&a->mut);
    pthread_mutex_unlock(&b->mut);
    return ret;
}

void* start_routine1(void* args)
{
    int money = (int) args;
    int ret = withdraw(&acct1, &acct2, money);
    if(ret != -1)
    {
        printf("0x%lx: accout1 transfer %d to account2\n", pthread_self(), ret);
    }
    return NULL;
}

void* start_routine2(void* args)
{
    int money = (int) args;
    int ret = withdraw(&acct2, &acct1, money);
    if(ret != -1)
    {
        printf("0x%lx: accout2 transfer %d to account1\n", pthread_self(), ret);
    }
    return NULL;
}

int main()
{
    pthread_t tid[2];
    //因为每个线程的逻辑不同,所以需要定义两个函数
    pthread_create(&tid[0], NULL, start_routine1, (void*)100);
    pthread_create(&tid[1], NULL, start_routine2, (void*)500);

    //直接传线程标号
    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);

    return 0;
}

运行上面的代码,你会发现进程出现了死锁现象:进程中的所有线程都陷入了阻塞状态,没有一个线程能继续往下执行。

0 为主线程,1为子线程,S表示sleep,都为S,大概率出现了死锁现象。

死锁产生的原因

死锁是一个被广泛研究过了的问题,它的产生必须同时满足下面四个条件,缺一不可:

  • 互斥:多个线程不可同时持有同一个资源。
  • 持有并等待:线程持有资源 1,想申请资源 2;而资源 2 被另一线程所持有,所以线程陷入阻塞;但是线程陷入阻塞之前并不会释放自己所持有的资源 1。
  • 不可抢占:当线程已经持有了资源,在使用完之前不能被其他线程获取。
  • 循环等待:线程和资源之间的关系形成了一条环路,如下图所示:

这样就形成了一个循环等待的环路,导致死锁发生。

解决方案

知道了死锁产生的原因,那么死锁的解决方案也就浮出水面了:破坏上面四个条件中的一种或某几种。

  • 破坏持有并等待:要么全部持有,要不全部不持有。
c 复制代码
  pthread_mutex_lock(&protection);

  pthread_mutex_lock(&acctA->mutex);
  sleep(1);  // 切换
  pthread_mutex_lock(&acctB->mutex);

  pthread_mutex_unlock(&protection);
  • 破坏不能抢占:主动放弃
c 复制代码
start:
    pthread_mutex_lock(&acctA->mutex);
    sleep(1);  // 切换
    int err = pthread_mutex_trylock(&acctB->mutex);
    if (err) {
        pthread_mutex_unlock(&acctA->mutex);
        // 停留一个随机的时间
        int nsec = rand() % 5;
        sleep(nsec);
        goto start;
    }
  • 破坏循环等待:按固定的顺序依次获取每一个锁
c 复制代码
if (acctA->id < acctB->id) {
    pthread_mutex_lock(&acctA->mutex);
    sleep(1);  // 切换
    pthread_mutex_lock(&acctB->mutex);
} else {
    pthread_mutex_lock(&acctB->mutex);
    sleep(1);  // 切换
    pthread_mutex_lock(&acctA->mutex);
}

不能够破坏互斥条件 -》可以不使用锁 -》lock-free 无锁编程 -》基于CPU指令 CAS(compare and swap)
总结 使用互斥锁应该注意的事项

  1. 在所有的退出点都要释放锁
  2. 使用合适粒度的锁
  3. 使用多个锁,要警惕死锁现象

条件变量

提供一种等待唤醒机制

初始化

条件变量的数据类型为 pthread_cond_t。类似于互斥量,使用条件变量前必须对其初始化。

c 复制代码
#include <pthread.h>

//还是全局变量或者局部静态变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 静态初始化,仅适用于静态变量
int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* attr); // 动态初始化
// 成功: 0
// 失败: 返回错误状态码,不会设置errno
等待

当条件不成立时,我们需要阻塞当前线程,等待条件成立。

cc 复制代码
#include <pthread.h>

int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);	//互斥锁,用来保护条件变量以及其他共享数据

int pthread_cond_timedwait(pthread_cond_t* cond, pthread_mutex_t* mutex, const struct timespec* abstime);
// 成功: 0
// 失败: 返回错误状态码,不会设置errno

pthread_cond_wait() 函数的语义如下:

  1. 释放所持有的互斥量 mutex
  2. 陷入阻塞状态。
  3. pthread_cond_wait() 返回时,调用线程一定再一次获取了互斥量 mutex

Question

pthread_cond_wait() 返回时,线程等待的条件 cond 一定成立吗?如果不一定成立,那 pthread_cond_wait() 的返回表达的意思又是什么呢?

不一定成立,曾经条件成立过,现在条件不一定成立!(虚假唤醒)
如果没有收到其它线程的通知,pthread_cond_wait() 会让调用线程一直阻塞;而 pthread_cond_timedwait() 可以指定一个超时时间,如果在超时时间内没有收到相关条件变量的通知,则返回错误码 ETIMEDOUT

通知 (唤醒)

当条件成立时,我们需要唤醒等待该条件的线程。

c 复制代码
#include <pthread.h>

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
// 成功: 0
// 失败: 返回错误状态码,不会设置errno

pthread_cond_signal() 函数保证至少唤醒一条等待 cond 的线程,而 pthread_cond_broadcast() 会唤醒所有等待 cond 的线程。

销毁

当不再需要自动或动态分配的条件变量时,应调用 pthread_cond_destroy() 函数予以销毁。对于使用 PTHREAD_COND_INITIALIZER 进行静态初始化的条件变量,则无需销毁。

c 复制代码
#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond);
// 成功: 0
// 失败: 返回错误状态码,不会设置errno

当后续不再使用条件变量时,才应调用 pthread_cond_destroy() 将其销毁。

pthread_cond_destroy() 销毁的条件变量,之后可以调用 pthread_cond_init() 对其重新初始化。

实际的应用场景

主线程接收请求,将请求转化为一个任务,放到阻塞队列,线程池中的线程等待阻塞队列的条件为非空,被唤醒,至少唤醒一个,假设唤醒1号与2号线程,一号线程拿到互斥锁,执行任务,完成任务释放互斥锁,二号线程此时也为唤醒状态,再次拿到互斥锁时,条件不成立。

条件变量等待队列的基本工作原理:

加入等待队列

  1. 检查条件和释放锁 :当一个线程调用 pthread_cond_wait() 函数时,它首先需要持有相关的互斥量(mutex)。在调用 pthread_cond_wait() 时,该函数会自动解锁这个互斥量,并将当前线程的状态设置为等待状态,然后将其加入到与条件变量关联的等待队列中。
  2. 进入等待状态:一旦线程被加入到等待队列中,它就会进入阻塞状态,暂停执行,直到接收到信号或广播通知。这意味着线程不会占用CPU资源,也不会继续执行,直到其被重新唤醒。

唤醒过程

  • pthread_cond_signal() :当另一个线程调用了 pthread_cond_signal() 函数时,系统会选择等待队列中的一个线程(通常是第一个)进行唤醒。然而,这只是意味着该线程有机会尝试重新获取互斥量并继续执行;如果此时有多个线程竞争同一个互斥量,那么可能并不是每次都能成功获取。
  • pthread_cond_broadcast() :与 pthread_cond_signal() 不同,pthread_cond_broadcast() 会唤醒等待队列中的所有线程。但是,所有被唤醒的线程都将试图重新获取互斥量,只有成功获得互斥量的线程才能真正继续执行。其他线程将继续处于阻塞状态,等待下一次机会获取互斥量。

线程重新激活后的处理

一旦线程被唤醒并成功重新获取了互斥量,它会从 pthread_cond_wait() 函数返回,并通常会在返回后立即检查条件是否真的已经满足(因为在多线程环境下,条件可能在唤醒后再次发生变化)。这是因为在唤醒之后,尽管原条件可能已满足,但其它线程也可能已经改变了共享资源的状态。

生产者消费者模型

生产者和消费者模型中有三个角色:

  • 生产者:生产商品。如果队列满了,生产者陷入阻塞,等待队列不满 (not_full);如果队列不满,将商品添加到队列中,此时队列不空 (not_empty),唤醒消费者。

  • 消费者:消费商品。如果队列空了,消费者陷入阻塞,等待队列不空 (not_empty);如果队列不空,从队列中删除商品,此时队列不满 (not_full),唤醒生产者。

  • 阻塞队列:当队列满时,如果线程往阻塞队列中添加商品,该线程阻塞;当队列空时,如果线程从阻塞队列中删除商品,该线程阻塞。

问题1. 线程池中线程数目应该设置为多少?

  1. 与CPU核数相关
  2. 与任务负载相关(CPU密集型任务 1:1, IO密集型任务 1:N (CPU核数的2或者3倍))
c 复制代码
// main.c

#include "blockq.h"

typedef struct{
    //线程池中线程的id,因为线程的操作都是通过线程id进行操作的
    pthread_t* pthread;
    int num;    //线程的数量
    BlockQ* q;  //一个阻塞队列

}Threadpool;

// 线程池的创建 + 初始化
Threadpool* Threadpool_create(int n)
{
    //开辟空间
    Threadpool* pool = (Threadpool*) malloc(sizeof(Threadpool));
    //开存在线程id的数组空间
    pool->pthread = (pthread_t*) malloc(n * sizeof(pthread_t));
    pool->num = n;
    pool->q = blockq_create();
    return pool;
}

void Thread_destory(Threadpool* pool)
{
    block_destory(pool->q);
    free(pool->pthread);
    free(pool);
}

void* start_routine(void* args)
{
    Threadpool* pool = (Threadpool*) args;

    pthread_t tid = pthread_self();
    for(;;)
    {
        int task_id = block_pop(pool->q);
        if(task_id == -1)
        {
            printf("0x%lx : task %d done\n", tid, task_id);
            pthread_exit(NULL);
        }
        printf("0x%lx : execute task %d\n", tid, task_id);
        sleep(1);
        printf("0x%lx : task %d done\n", tid, task_id);
    }
    return NULL;
}

int main()
{
    Threadpool* pool = Threadpool_create(8);
    //创建线程
    for(int i = 0; i < pool->num; i++)
    {
        pthread_create(pool->pthread + i, NULL, start_routine, pool);
    }
    //模拟向阻塞队列中添加任务
    for(int i = 1; i <= 100; i++)
    {
        block_push(pool->q, i);
    }

    sleep(10);
    
    //退出
    //1. 强制退出,发送取消请求
    // for(int i = 0; i < pool->num; i++)
    // {
    //     pthread_cancel(pool->pthread[i]);
    // }
    //2.发送特殊的任务
    for(int i = 0; i < pool->num; i++)
    {
        block_push(pool->q, -1);
    }
    //等待 有序退出
    for(int i =0; i < pool->num; i++)
    {
        pthread_join(*(pool->pthread+i), NULL);
    }

    Thread_destory(pool);
    return 0;
}
c 复制代码
//blockq.h

#pragma

#include <stdio.h>
#include <pthread.h>
#include <error.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdbool.h>

#define N 1024
typedef int E;

//阻塞队列 结构体设置
typedef struct {
    E elements[N];  //固定长度的数组,防止需求很多时,占用内存太多,操作系统将进程杀死
    int front;  //队列的头
    int rear;   //下一个数据的存储位置
    int size;   //阻塞队列的长度
    pthread_mutex_t mut;
    pthread_cond_t not_empty;
    pthread_cond_t not_full;
}BlockQ;

BlockQ* blockq_create(void);
void block_destory(BlockQ* q);

bool block_empty(BlockQ* q);
bool block_full(BlockQ* q);
void block_push(BlockQ* q, E val);
E block_pop(BlockQ* q);
E block_peek(BlockQ* q);
c 复制代码
// blockq.c

#include "blockq.h"

BlockQ* blockq_create(void)
{
    //对堆上malloc空间
    BlockQ* q = (BlockQ*) malloc(sizeof(BlockQ));

    q->front = 0;
    q->rear = 0;
    q->size = 0;
    
    //初始化互斥量
    pthread_mutex_init(&q->mut, NULL);
    
    //初始化条件变量
    pthread_cond_init(&q->not_empty, NULL);
    pthread_cond_init(&q->not_full, NULL);
    return q;
}

void block_destory(BlockQ* q)
{
    //回收空间,一般按照初始化相反的顺序进行回收
    pthread_cond_destroy(&q->not_full);
    pthread_cond_destroy(&q->not_empty);
    pthread_mutex_destroy(&q->mut);
    free(q);
}

bool block_empty(BlockQ* q)
{
    //因为要对数据进行查看,所以要拿到互斥锁
    pthread_mutex_lock(&q->mut);
    E sz = q->size;
    pthread_mutex_unlock(&q->mut);
    return sz == 0;
}
bool block_full(BlockQ* q)
{
    pthread_mutex_lock(&q->mut);
    E sz = q->size;
    pthread_mutex_unlock(&q->mut);
    return sz == N;
}
void block_push(BlockQ* q, E val)
{
    //上锁
    pthread_mutex_lock(&q->mut);
    while(q->size == N)
    {
        //not_full 条件不存在
        //如果为满进入阻塞状态
        //释放所持的互斥量
        //陷入阻塞状态
        pthread_cond_wait(&q->not_full, &q->mut);
        //当函数返回时,拿到互斥量
        //可能为虚假唤醒
    }//一定满足条件阻塞队列不满
    q->elements[q->rear] = val;
    q->rear = (q->rear + 1)%N;
    q->size ++;

    //一定不为满
    pthread_cond_signal(&q->not_empty);
    pthread_mutex_unlock(&q->mut);
}
E block_pop(BlockQ* q)
{
    //上锁
    pthread_mutex_lock(&q->mut);
    while(q->size == 0)
    {
        //not_empty 条件不存在
        //进入阻塞状态,等待条件变量
        //释放所持的互斥量
        //陷入阻塞状态
        pthread_cond_wait(&q->not_empty, &q->mut);
        //当函数返回时,拿到互斥量
        //可能为虚假唤醒
    }//一定满足
    E val = q->elements[q->front];
    q->front = (q->front+1)%N;
    q->size--;
    pthread_cond_signal(&q->not_full);
    pthread_mutex_unlock(&q->mut);
    return val;
}

E block_peek(BlockQ* q)
{
    //上锁
    pthread_mutex_lock(&q->mut);
    while(q->size == 0)
    {
        //不为空不满足
        pthread_cond_wait(&q->not_empty, &q->mut);
    }
    E val = q->elements[q->front];
    pthread_mutex_unlock(&q->mut);
    return val;
}