Linux 多线程编程详解:从基础概念到同步机制

一、概念

**进程:**一个动态的概念,本质是一个程序正在执行的序列,是程序运行的载体。

线程:隶属于进程,是进程内部的一条执行路径,也是进程内部的一个执行序列。一个进程可以包含多个线程,多个线程共享进程的资源,协同完成进程的任务。

Linux中线程的实现

与其他操作系统不同,在内核的角度来讲,Linux 系统中并没有专门的"线程"概念。

Linux 内核将所有的线程都当作进程来实现和管理,既没有为线程设计专门的调度算法,也没有定义专门表征线程的数据结构。

在 Linux 内核中,线程仅仅被视为一个与其他进程共享某些资源的特殊进程。每个进程(包括被当作线程的进程)都拥有唯一隶属于自己的 task_struct 结构体(进程控制块),因此从内核视角来看,线程和普通进程没有本质区别,唯一的不同是:线程会与其他一些进程共享地址空间、打开的文件等资源,而普通进程拥有独立的资源空间。

进程和线程的区别

  1. 本质区别:进程是程序正在执行的过程,是动态概念;线程是进程内部的一条执行路径或执行序列,是进程的组成部分。
  2. 资源分配:进程是资源分配的最小单位,拥有独立的地址空间和系统资源;线程是 CPU 调度的最小单位,不拥有独立的资源,共享所属进程的地址空间和资源。
  3. 开销差异:进程的创建、切换开销较大(需分配独立资源、切换进程控制块);线程的创建、切换开销相对较小(无需分配新资源,仅切换线程执行上下文)。

二、线程的接口相关函数

线程的操作依赖于 pthread 库提供的接口,以下是最常用的三个核心接口,用于线程的创建、等待和退出。

pthread_create

创建一个新的线程,并指定线程的入口函数,新线程创建成功后会立即执行入口函数。

cpp 复制代码
int pthread_create(
    pthread_t *thread, 
    const pthread_attr_t *attr,
    void *(*start_routine) (void *), 
    void *arg
);
  • thread:输出型参数,用于接收创建成功后的线程 ID,后续操作(如等待线程退出)需通过该 ID 定位线程。
  • attr:用于设置线程的属性(如线程优先级、栈大小等),一般无需特殊设置,直接传 NULL 即可(使用默认属性)。
  • start_routine:线程入口函数的指针,该函数的返回值和参数均为 void* 类型,是新线程启动后执行的核心逻辑。
  • arg:传递给线程入口函数的参数,若无需传递参数,可传 NULL;若传递多个参数,需封装为结构体指针传入。
  • 返回值:函数执行成功返回 0;失败返回对应的错误码(注意:不是 errno,需通过错误码判断失败原因)。

pthread_join

阻塞等待指定的线程退出,直到该线程执行完毕后,当前线程(通常是主线程)才会继续执行,同时可以接收线程退出时的返回信息。

cpp 复制代码
int pthread_join(pthread_t thread, void **retval);  
  • thread: 要等待的线程的 ID,即 pthread_create 函数返回的线程 ID
  • retval:输出型参数,用于接收线程退出时通过 pthread_exit 传递的退出信息(本质是一个 void* 类型的指针)。

pthread_exit

用于在子线程内部主动退出线程,同时可以指定退出信息,该信息可被 pthread_join 函数接收。

cpp 复制代码
int pthread_exit(void *retval);
  • retval:用于指定线程的退出信息,传递给 pthread_join 函数的 retval 参数。
  • 注意:不能返回临时变量的地址。因为临时变量存储在栈上,线程退出后,栈空间会被释放,临时变量的地址会变成无效地址,若通过 retval 接收该地址,会导致程序异常。

三、使用示例

核心注意点

  1. 如果主进程不使用 pthread_join 等待子线程退出,可能会出现"线程函数还未执行完毕,主进程就已经退出"的情况。主进程退出后,其所属的所有子线程会被强制终止,导致线程逻辑无法完整执行。
  2. pthread_exit 的作用:用于线程自身主动退出,并传递退出信息,通常在子线程中使用。
  3. pthread_join 的额外作用:除了等待线程退出、接收退出信息外,还会释放子线程占用的系统资源,避免资源泄漏。
  4. 主线程并非必须调用 pthread_join:只要主线程不提前退出(如通过循环、阻塞等方式保持运行),子线程可以正常执行完毕,此时无需调用 pthread_join。

示例代码

cpp 复制代码
#include  <stdio.h>
#include  <stdlib.h>
#include <pthread.h>
#include <unistd.h>
// 线程入口函数
void * thread_fun(void *arg)
{
     for(int i=0;i<10;i++)
     {
        printf("fun run\n");
        sleep(1);
     }
     pthread_exit("fun over\n");// 子线程退出,并传递退出信息
}

int main()
{
    pthread_t id;// 存储线程ID
    // 创建子线程:无属性、无参数
    pthread_create(&id,NULL,thread_fun,NULL);

    for(int i=0;i<5;i++)
    {
        printf("main run\n");
        sleep(1);
    }

    char *s=NULL;
    pthread_join(id,(void **)&s);// 等到子线程退出,并接收退出信息
    printf("join:s=%s\n",s);// 打印退出信息
    exit(0);
}

如果不使用pthread_join的运行结果

若删除 main 函数中的 pthread_join 语句,主线程会在循环 5 次(5 秒)后直接退出。此时子线程还在执行(需执行 10 次,共 10 秒),但由于主进程退出,子线程会被强制终止,无法完成后续的打印操作,最终只能看到 5 次"main run"和 5 次左右的"fun run"(具体次数取决于线程调度),无法看到"fun over"的输出。

四、线程同步

相关概念

a. 同步概念

同一个进程中的所有线程,共享该进程的地址空间以及进程拥有的其他资源 (如打开的文件、全局变量等)。那么,一个线程对资源的修改会影响其他线程的运行环境,因此需要通过**线程同步,**对多个线程的执行顺序进行控制,让线程按照预定的规则执行,从而保证线程运行的安全性和效率。

同步:让线程按规则执行,保证安全与效率。

**线程同步:**一个线程在操作临界资源时,其他线程必须等待,直到操作完成。

b. 关键名词

临界资源:同一时刻,只允许被一个进程或一个线程访问的资源。例如打印机、全局变量等

临界区:程序中访问临界资源的那段代码段。线程同步的核心就是保护临界区,确保同一时刻只有一个线程进入临界区执行操作。

c. 线程同步方法

为了实现线程同步,Linux 提供了多种机制,最常用的四种方法如下:

  1. 互斥锁:最基础、最常用的同步方式,保证同一时刻只有一个线程进入临界区。
  2. 信号量:用于控制资源的可用数量,可实现多线程对有限资源的有序访问。
  3. 条件变量:用于实现线程等待某个条件成立,避免线程频繁检查条件而浪费 CPU 资源。
  4. 读写锁:区分读操作和写操作,适用于"读多写少"的场景,提高并发效率。

1. 互斥锁

a. 核心接口

互斥锁的使用遵循"初始化→加锁→解锁→销毁"的流程,核心接口如下:

pthread_mutex_init

初始化锁

cpp 复制代码
int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr);
  • mutex:执行互斥锁变量的指针
  • attr:用于设置互斥锁的属性,不需要特殊设置,传空即可
pthread_mutex_lock

加锁:尝试获取互斥锁,若锁未被占用,则当前线程获取锁并进入临界区;若锁已被其他线程占用,则当前线程阻塞,直到锁被释放。

cpp 复制代码
int pthread_mutex_lock(pthread_mutex_t *mutex); 
pthread_mutex_unlock

解锁:当前线程释放互斥锁,若有其他线程因获取该锁而阻塞,会唤醒其中一个线程获取锁。

cpp 复制代码
int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_destroy

销毁锁:释放互斥锁占用的系统资源,需在所有使用该互斥锁的线程全部结束后调用。

cpp 复制代码
int pthread_mutex_destroy(pthread_mutex_t *mutex);  

注意:互斥锁的所有接口中,mutex 参数都需要传递地址,因为接口内部会修改互斥锁的状态(如锁的占用情况),传递地址才能实现状态的修改。

b. 使用示例

i. 解决多线程并发计数问题

场景:5 个线程同时对全局变量 wg 进行 1000 次自增操作,若不加锁,由于 wg++ 不是原子操作(本质是"读取-修改-写入"三步操作),多线程会相互干扰,导致最终结果小于 5000;加锁后,可保证每次自增操作的原子性,最终结果为 5000(正确值)。

不加锁代码

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

int wg=0;

void* func(void* arg){
    for(int i=0;i<1000;i++){
        wg++;
    }
}

int main(){
    pthread_t id[5] = {0};
    for(int i = 0; i < 5; i++){
        int res = pthread_create(&id[i], NULL, func, NULL);
        if(res != 0){
            printf("pthread create failed\n");
            return -1;
        }
    }

    for(int i=0;i<5;i++){
        pthread_join(id[i], NULL);
    }
    printf("wg = %d\n", wg);
    return 0;
}

运行结果:多次运行,会发现有些结果并不是5000。这是因为多个线程同时执行 wg++ 时,会出现"多个线程读取到同一个 wg 值,同时自增,最终只实现一次有效自增"的情况,导致数据混乱。

加锁方式 1:锁整个循环

效率较低,一次只允许一个线程执行全部 1000 次自增

cpp 复制代码
pthread_mutex_lock(&mutex);
for(int i=0;i<1000;i++){
    wg++;
}
pthread_mutex_unlock(&mutex);

加锁方式 2:锁单次操作

效率较高,多个线程可交替执行自增,仅保证单次自增原子性

cpp 复制代码
for(int i=0;i<1000;i++){
    pthread_mutex_lock(&mutex);
    wg++;
    pthread_mutex_unlock(&mutex);
}

加锁原理:加锁后,无论哪种方式,都能将 wg++ 操作变成原子操作,即同一时刻只有一个线程能执行 wg++,避免了线程之间的干扰,最终保证 wg 的值为 5000。

ii. 模拟共享打印机(互斥访问)

场景:主线程和子线程模拟访问同一台打印机,主线程输出两个字符'A'(分别表示开始使用和结束使用打印机),子线程输出两个字符'B'(逻辑与主线程一致)。由于打印机是临界资源,同一时刻只能被一个线程使用,因此输出结果不能出现 ABAB 交替的情况(即不能出现"一个线程未结束使用,另一个线程就开始使用")。

通过互斥锁,可保证同一时刻只有一个线程访问打印机(临界资源),实现互斥访问。

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

// 打印机模拟
pthread_mutex_t mutex;

void* func(void* arg){
    for(int i = 0; i < 5; i++){
        pthread_mutex_lock(&mutex);// 加锁
        printf("B\n");
        sleep(2);// 模拟打印机
        printf("B\n");
        pthread_mutex_unlock(&mutex);// 解锁
        sleep(1);
    }
}

int main(){
    // 需要在创建线程之前进行初始化
    pthread_mutex_init(&mutex, NULL);// 初始化锁

    pthread_t id;
    int res = pthread_create(&id, NULL, func, NULL);
    if(res != 0){
        printf("pthread create failed\n");
        return -1;
    }

    for(int i = 0; i < 5; i++){
        pthread_mutex_lock(&mutex);// 加锁
        printf("A\n");
        sleep(2);// 模拟打印机
        printf("A\n");
        pthread_mutex_unlock(&mutex);// 解锁
        sleep(1);
    }

    pthread_join(id, NULL);// 等待线程结束
    pthread_mutex_destroy(&mutex);// 没有线程在使用锁,才能删除
    return 0;
}

2.信号量

信号量通过控制资源的可用数量,实现多个线程对临界资源的有序访问。其核心类型为 sem_t,通常定义为全局变量(方便多线程共享访问),使用前需包含指定头文件。

a.核心接口

头文件

cpp 复制代码
#include<semaphore.h>
sem_init

用于初始化信号量的初始值和共享属性,是使用信号量的第一步。

cpp 复制代码
int sem_init(sem_t *sem, int pshared, unsigned int value);
  • sem:指向信号量变量的指针,需传入地址(因要修改信号量状态)。
  • pshared:设置信号量是否在进程间共享,Linux 系统不支持进程间共享信号量,因此该参数固定传0(非 0 表示进程间共享,无效)
  • value:信号量的初始值,代表可用资源的数量(核心参数)。例如 value=1 时,等价于互斥锁,实现线程互斥;value > 1 时,可实现多线程并发访问有限资源。
sem_wait

P操作,等待获取资源

  1. 若信号量值大于 0,将其减 1(表示占用一个资源),函数立即返回;
  2. 若信号量值为 0,函数阻塞,直到有其他线程释放资源(执行 V 操作)。
cpp 复制代码
int sem_wait(sem_t *sem);
sem_post

V操作,释放一个资源,将信号量值加 1,若有线程因调用 sem_wait 阻塞,会唤醒其中一个线程获取资源。

cpp 复制代码
int sem_post(sem_t *sem);
sem_destroy

销毁信号量,用于释放信号量占用的系统资源,必须在所有使用该信号量的子线程全部结束后调用,否则会导致资源泄漏或程序异常。

cpp 复制代码
int sem_destroy(sem_t *sem);

b.使用示例

i. 解决多线程并发计数问题

场景:5个线程同时对全局变量 wg 进行 1000 次自增操作,通过信号量保证自增操作的原子性,避免数据混乱(与互斥锁功能类似,此处信号量初始值为 1,等价于互斥锁)。

cpp 复制代码
#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>

sem_t sem;// 全局变量
int wg=0;

void* func(void* arg){
    for(int i=0;i<1000;i++){
        sem_wait(&sem);// P操作
        wg++;
        sem_post(&sem);// V操作
    }
}

int main(){
    sem_init(&sem, 0, 1);// 初始化
    pthread_t id[5] = {0};
    for(int i = 0; i < 5; i++){
        int res = pthread_create(&id[i], NULL, func, NULL);
        if(res != 0){
            printf("pthread create failed\n");
            return -1;
        }
    }

    for(int i=0;i<5;i++){
        pthread_join(id[i], NULL);
    }
    printf("wg = %d\n", wg);
    sem_destroy(&sem);// 销毁信号量
    return 0;
}
ii. 实现读写线程同步

场景:主线程从键盘获取用户输入,子线程将输入内容写入文件,通过两个信号量控制读写顺序,保证"先读(输入)、后写(写入文件)",避免读写混乱。

核心逻辑:sem1 控制主线程输入(初始值1,允许先输入),sem2 控制子线程写入(初始值0,等待输入完成后再写入),形成"输入→写入"的同步流程。

cpp 复制代码
#include<stdio.h>
#include<string.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<pthread.h>
#include<semaphore.h>

// 2.主线程获取用户输入,函数线程将用户输入的数据存储到文件中
// 需要两个信号量:保证读写的同步
sem_t sem1;
sem_t sem2;
char buff[128] = {0};

void* func(void* arg){
    int fd = open("a.txt", O_RDWR | O_CREAT, 0644);// 打开文件
    if(fd == -1){
        printf("open failed\n");
        return NULL;
    }

    while(1){
        sem_wait(&sem2);
        if(strncmp(buff, "end", 3) == 0){
            break;
        }
        write(fd,buff,strlen(buff));// 将buff内容写入文件
        memset(buff, 0 ,128);// 重置buff内容
        sem_post(&sem1);
    }

    close(fd);// 关闭文件
}
int main(){
    sem_init(&sem1, 0, 1);
    sem_init(&sem2, 0, 0);

    pthread_t id;
    pthread_create(&id, NULL, func, NULL);

    while(1){
        sem_wait(&sem1);
        printf("please input:");
        fflush(stdout);// 刷新缓冲区

        fgets(buff, 128, stdin);// 从标准输入获取
        buff[strlen(buff)-1]='\0';// 主线程写入buff
        sem_post(&sem2);
        if(strncmp(buff, "end", 3) == 0){
            break;
        }
    }

    pthread_join(id, NULL);

    sem_destroy(&sem1);
    sem_destroy(&sem2);
    return 0;
}
iii. 实现线程顺序打印ABC

场景:创建3个线程,分别打印A、B、C,要求严格按照ABC的顺序循环打印5次。结合表驱动思想(用函数数组替代条件判断),简化线程创建逻辑,通过3个信号量控制线程执行顺序。

表驱动

核心特点:替代大量 if/switch 判断,代码更简洁;数据与逻辑分离,新增功能只需修改函数数组,无需改动核心逻辑;通过索引快速定位函数,效率更高。

cpp 复制代码
typedef void* (*Func)(void*);
Func funcArr[]={
    func1,
    func2,
    func3
};

pthread_t id[3];
for(int i = 0; i < 3; i++){
    int res = pthread_create(&id[i], NULL, funcArr[i], NULL);
    assert(res != -1);
}
cpp 复制代码
#include<stdio.h>
#include<string.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<assert.h>
#include<pthread.h>
#include<semaphore.h>

// 3.循环打印ABC(顺序必须是ABC)
sem_t sem1;
sem_t sem2;
sem_t sem3;

void* func1(void* arg){
    int count = 5;
    while(count--){
        sem_wait(&sem1);
        printf("A\n");
        sleep(1);
        sem_post(&sem2);
    }
}
void* func2(void* arg){
    int count = 5;
    while(count--){
        sem_wait(&sem2);
        printf("B\n");
        sleep(1);
        sem_post(&sem3);
    }
}
void* func3(void* arg){
    int count = 5;
    while(count--){
        sem_wait(&sem3);
        printf("C\n");
        sleep(1);
        sem_post(&sem1);
    }
}

typedef void* (*Func)(void*);
Func funcArr[]={
    func1,
    func2,
    func3
};

int main(){
    sem_init(&sem1, 0, 1);
    sem_init(&sem2, 0, 0);
    sem_init(&sem3, 0, 0);

    pthread_t id[3];
    for(int i = 0; i < 3; i++){
        int res = pthread_create(&id[i], NULL, funcArr[i], NULL);
        assert(res != -1);
    }

    for(int i = 0; i < 3; i++){
        pthread_join(id[i], NULL);
    }
    sem_destroy(&sem1);
    sem_destroy(&sem2);
    sem_destroy(&sem3);
    return 0;
}

3.条件变量

用于实现"线程等待某个条件成立"的场景,通常与互斥锁配合使用,避免线程因频繁检查条件而浪费CPU资源(阻塞等待,条件成立时被唤醒)。其核心类型为 pthread_cond_t,需定义为全局变量,使用前包含指定头文件。

a.核心接口

cpp 复制代码
#include<pthread.h>
pthread_cond_init

初始化条件变量

cpp 复制代码
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr);
  • cond:指向条件变量的指针,需传入地址
  • attr:条件变量的属性,一般无需设置,传 NULL 即可(使用默认属性)。
pthread_cond_wait

阻塞等待条件成立

  1. 将当前线程加入条件变量的等待队列,同时自动释放互斥锁(避免死锁);
  2. 当条件成立被唤醒时,线程会重新获取互斥锁,然后继续执行。
cpp 复制代码
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
  • cond:指向条件变量的指针。
  • mutex:互斥锁指针,必须与条件变量配合使用(核心:避免多线程同时检查条件的竞态问题)。
pthread_cond_signal

唤醒单个线程:从条件变量的等待队列中,唤醒一个阻塞的线程(随机唤醒一个),适用于"只要有一个线程处理即可"的场景。

cpp 复制代码
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_broadcast

唤醒所有线程:唤醒条件变量等待队列中的所有阻塞线程,适用于"所有线程都需要处理条件"的场景(如程序退出时,唤醒所有等待线程)。

cpp 复制代码
int pthread_cond_broadcast(pthread_cond_t *cond);
pthread_cond_destroy

销毁条件变量:用于释放条件变量占用的系统资源,需在所有使用该条件变量的线程全部结束后调用,且调用前需确保没有线程处于等待状态。

cpp 复制代码
int pthread_cond_destroy(pthread_cond_t *cond);

b.使用示例

场景:主线程从键盘获取用户输入,将输入内容存入缓冲区 buff(条件成立);创建两个工作线程,阻塞等待条件成立,被唤醒后打印缓冲区内容;输入"end"时,唤醒所有工作线程,程序退出。

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<assert.h>
#include<pthread.h>

pthread_cond_t cond;
pthread_mutex_t mutex;
char buff[128] = {0};

void* work_thread(void* arg){
    while(1){
        char* thread_name = (char*)arg;// 线程名
        pthread_mutex_lock(&mutex);// 加锁
        pthread_cond_wait(&cond, &mutex);// 等待目标条件变量
        pthread_mutex_unlock(&mutex);// 解锁

        if(strncmp(buff, "end", 3) == 0){
            break;
        }
        
        printf("%s: %s", thread_name, buff);
    }
}

int main(){
    pthread_cond_init(&cond, NULL);
    pthread_mutex_init(&mutex, NULL);

    pthread_t id1,id2;
    int res1 = pthread_create(&id1, NULL, work_thread, "thread1");
    assert(res1 == 0);
    int res2 = pthread_create(&id2, NULL, work_thread, "thread2");
    assert(res2 == 0);

    while(1){
        fgets(buff, 127, stdin);
        if(strncmp(buff, "end", 3)==0){
            // 唤醒所有线程
            pthread_cond_broadcast(&cond);
            break;
        }else{
            // 唤醒一个线程
            pthread_cond_signal(&cond);
        }
    }

    pthread_join(id1, NULL);
    pthread_join(id2, NULL);
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);
    return 0;
}

4.读写锁

读写锁是一种更灵活的同步机制,区分"读操作"和"写操作",适用于"读多写少"的场景(如数据查询多、修改少),能提高并发效率。其核心类型为 pthread_rwlock_t,使用前需包含指定头文件。

a.核心接口

头文件:#include <pthread.h>

pthread_rwlock_init

初始化读写锁:第一个参数是锁的地址,第二个参数是锁的属性,一般写NULL

cpp 复制代码
int pthread_rwlock_init(pthread_rwlock_t *rwlock, pthread_rwlockattr_t *attr);
  • rwlock:指向读写锁的指针,需传入地址。
  • attr :读写锁的属性,一般无需设置,传 NULL 即可(使用默认属性)。
pthread_rwlock_rdlock

加读锁:为读操作加锁,多个线程可同时加读锁(读操作并发执行);若当前有线程加了写锁,则读锁会阻塞,直到写锁释放。

cpp 复制代码
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_wrlock

加写锁:为写操作加锁,同一时刻只能有一个线程加写锁(写操作互斥);若当前有线程加了读锁或写锁,则写锁会阻塞,直到所有锁释放。

cpp 复制代码
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
pthread_rwlock_unlock

解锁:无论加的是读锁还是写锁,都通过该函数解锁

cpp 复制代码
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
pthread_relock_destroy

销毁:用于释放读写锁占用的系统资源,需在所有使用该读写锁的线程全部结束后调用,且调用前需确保读写锁处于未加锁状态(避免资源泄漏)。

cpp 复制代码
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)

b.使用示例(读多写少场景)

场景:创建3个线程,2个线程执行读操作(并发),1个线程执行写操作(互斥),模拟"读多写少"场景,验证读写锁的特性:多个读线程可同时执行,读写线程互斥,写线程执行时读线程阻塞。

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<pthread.h>

pthread_rwlock_t rwlock;// 读写锁变量

void* w_thread(void* arg){
    // 写进程
    for(int i=0;i<5;i++){
        char* s = (char*)arg;
        pthread_rwlock_wrlock(&rwlock);// 加写锁
        printf("%s:  start\n",s);
        int n=rand()%3;
        sleep(n);
        printf("%s:  end\n",s);
        pthread_rwlock_unlock(&rwlock);// 解锁
        n=rand()%3;
        sleep(n);
    }
}
void* r_thread(void* arg){
    for(int i=0;i<5;i++){
        char* s = (char*)arg;
        pthread_rwlock_rdlock(&rwlock);// 加读锁
        printf("%s:  start\n",s);
        int n=rand()%3;
        sleep(n);
        printf("%s:  end\n",s);
        pthread_rwlock_unlock(&rwlock);// 解锁
        n=rand()%3;
        sleep(n);
    }
}
int main(){
    // 读写锁的初始化
    pthread_rwlock_init(&rwlock, NULL);

    pthread_t id[3];
    int res1 = pthread_create(&id[0], NULL, w_thread, "w_thread");
    assert(res1 == 0);
    int res2 = pthread_create(&id[1], NULL, r_thread, "r_thread1");
    assert(res2 == 0);
    int res3 = pthread_create(&id[2], NULL, r_thread, "r_thread2");
    assert(res3 == 0);


    pthread_join(id[0], NULL);
    pthread_join(id[1], NULL);

    // 删除读写锁
    pthread_rwlock_destroy(&rwlock);
    return 0;
}

运行情况说明:两个读线程可以同时打印"start"和"end"(并发执行);而写线程执行时,两个读线程会阻塞,直到写线程解锁;读线程执行时,写线程也会阻塞,直到所有读线程解锁,完全符合读写锁的特性。

5.自旋锁

自旋锁是 Linux 中轻量级同步机制,与互斥锁功能类似但实现逻辑不同。

核心定义 :自旋锁让等待锁的线程持续忙等(自旋)而非休眠阻塞,直到获取锁,适用于锁持有时间极短的场景。

核心特性

  1. 忙等不阻塞:获取锁失败时,线程不放弃 CPU,循环检查锁状态,CPU 会持续占用。
  2. 适用短临界区:仅当临界区执行时间<线程上下文切换开销时使用,否则 CPU 浪费严重。
  3. 不可休眠场景:内核中断处理程序等不能休眠的场景,只能用自旋锁(互斥锁会导致死锁)。

与互斥锁的关键区别

特性 自旋锁 互斥锁
等待方式 忙等(自旋) 休眠阻塞
CPU 消耗 高(自旋时占用 CPU) 低(休眠释放 CPU)
适用场景 锁持有时间极短、不可休眠 锁持有时间较长

核心接口

头文件<pthread.h>,核心函数如下:

cpp 复制代码
// 初始化:pshared指定进程共享属性(PRIVATE/SHARED)
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
// 销毁
int pthread_spin_destroy(pthread_spinlock_t *lock);
// 加锁(忙等)
int pthread_spin_lock(pthread_spinlock_t *lock);
// 尝试加锁(失败立即返回EBUSY)
int pthread_spin_trylock(pthread_spinlock_t *lock);
// 解锁
int pthread_spin_unlock(pthread_spinlock_t *lock);

使用注意事项

  1. 禁止长时间持有:否则会持续占用 CPU,导致其他线程自旋等待过久。
  2. 不可调用休眠函数:持有自旋锁时,不能调用sleep/read等可能让线程休眠的函数。
  3. 不支持重入:同一线程对已持有的自旋锁再次加锁,会导致未定义行为(如死锁)。
  4. 返回值检查:加锁失败会返回错误码(如EDEADLK死锁、EBUSY锁被占用),需处理。

五、线程安全性

1. 定义

线程安全是指:在多线程运行环境中,不论线程的调度顺序如何,多个线程同时调用同一个函数或访问同一个资源时,最终的结果始终是正确的、一致的,不会出现数据混乱、逻辑错误或程序崩溃。

2. 不安全的原因

核心原因是"竞态条件":多个线程同时访问和修改共享资源(如全局变量、静态变量),且访问/修改操作不是原子操作,导致线程之间相互干扰,出现数据错误。

只要使用了全局变量或静态变量的函数,大多是线程不安全的(不可重入函数)。

不可重入函数:当函数被多个线程反复调用时,会产生错误的结果(因共享了非局部变量)。

3. 保证安全的方法

  1. 实现线程同步:通过互斥锁、信号量、条件变量、读写锁等机制,保证同一时刻只有一个线程访问临界资源(临界区操作原子化)。

  2. 使用线程安全的函数(可重入函数):这类函数不使用全局变量、静态变量,仅使用局部变量或通过参数传递数据,多个线程同时调用不会产生竞态条件。

  3. 避免滥用全局变量:尽量使用局部变量,若必须使用共享变量,需通过同步机制保护。

4. 死锁(重点)

概念:两个或多个线程(进程)在执行过程中,因争夺临界资源或程序推进顺序不当,导致相互等待、永久阻塞的现象。一旦发生死锁,线程会一直阻塞,无法继续执行,程序也无法正常退出。

产生的原因主要有:

  1. 系统资源不足:多个线程争夺有限的资源(如信号量初始值太小),导致部分线程无法获取资源而阻塞,进而引发相互等待。
  2. 程序推进顺序不当:线程获取资源和释放资源的顺序不合理(如线程A先获取锁1,线程B先获取锁2,然后两者都尝试获取对方已持有的锁)。
  3. 资源分配不当:资源分配策略不合理,导致资源无法被及时释放,多个线程长期占用资源并等待其他资源。

死锁产生的必要条件:

  1. 互斥条件:临界资源只能被一个线程(进程)占用,其他线程(进程)只能等待,无法同时访问。
  2. 请求和保持条件:线程(进程)获取部分资源后,又尝试获取其他资源,同时不释放已持有的资源。
  3. 不剥夺条件:线程(进程)已获取的资源,不能被其他线程(进程)强制剥夺,只能由自身主动释放。
  4. 环路等待条件:多个线程(进程)之间形成循环等待链,每个线程(进程)都在等待下一个线程(进程)释放资源。

死锁的处理方法:

  1. 预防死锁:破坏死锁产生的任意一个必要条件(最常用,如按固定顺序获取资源、一次性获取所有资源)。
  2. 避免死锁:在资源分配前,通过算法判断是否会产生死锁(如银行家算法),若可能产生死锁,则拒绝分配资源。
  3. 检测死锁:通过系统工具或自定义算法,检测系统中是否存在死锁(如资源分配图)。
  4. 解除死锁:当检测到死锁后,采取措施释放资源、撤销线程(进程),打破死锁循环(如剥夺死锁线程的资源、撤销优先级最低的线程)。

六、总结

线程同步是多线程编程的核心,用于解决多线程共享资源时的数据竞争问题,保证程序运行结果正确、稳定。

1. 核心目的

  • 让多线程按规则、有序地访问临界资源
  • 避免数据混乱、逻辑错误、程序崩溃
  • 保证线程安全

2. 四大同步机制对比

同步机制 核心作用 典型场景 特点
互斥锁 同一时刻只允许一个线程访问资源 打印机、变量自增、独占资源 简单通用、完全互斥
信号量 控制可用资源的数量 顺序执行、有限资源并发 可控制并发数、可实现同步
条件变量 线程等待某个条件成立再执行 生产者消费者、数据就绪通知 不浪费 CPU、必须配合互斥锁
读写锁 读共享、写互斥 读多写少、配置文件、缓存 读并发高、写独占
相关推荐
Demon1_Coder2 分钟前
智能体的自定义工具
java·linux·前端
gf132111110 分钟前
【精确查找python脚本是否在运行】
linux·前端·python
Sunny Boy 00114 分钟前
linux环境编译Pro*C 源文件(.pc文件)
linux·c语言·oracle
用户9378558087037 分钟前
Linux 基础教程(二)】系统目录结构、用户与用户组管理(useradd/usermod/passwd/sudo)
linux
着迷不白1 小时前
实战一:用户、权限、组 案例
linux·运维
TheSumSt1 小时前
日常教程丨远程串流打游戏方法介绍(Parsec/Tailscale+Headscale+DERP+Sunshine&Moonlight)
linux·网络·经验分享·nginx·开源·玩游戏
暂未成功人士!1 小时前
ROS 核心知识点和常用的命令行详细总结
linux·操作系统·ros
念恒123061 小时前
进程间通信
linux·服务器·网络
tang7451639621 小时前
Huawei Cloud EulerOS 2.0(x8664)安装OpenJDK 2120260323
linux·运维·centos
2301_777998341 小时前
基础IO:IO操作&&重定向
linux·c语言