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、必须配合互斥锁
读写锁 读共享、写互斥 读多写少、配置文件、缓存 读并发高、写独占
相关推荐
男孩李2 小时前
浅谈Linux上安装 PostgreSQL数据库
linux·运维·服务器
Qt程序员2 小时前
深入理解 Linux 内核 RCU 机制:从原理到实现
linux·c++·内核·linux内核·rcu
钝挫力PROGRAMER2 小时前
Linux systemd服务获取不到用户环境变量
linux·运维·python
Magic--2 小时前
Linux exec进程替换详解
linux·运维·服务器
道阻且长行则将至!2 小时前
Linux 轻量级桌面环境
linux·运维·服务器·桌面管理器·ubuntu轻量级桌面
Trouvaille ~2 小时前
【项目篇】从零手写高并发服务器(十):性能测试与项目总结
linux·运维·c++·reactor·性能测试·高并发服务器·webbench
C++ 老炮儿的技术栈2 小时前
Tcp客户端报错原因分析
linux·c语言·网络·c++·网络协议·tcp/ip
co_wait2 小时前
【C语言】Linux系统文件操作函数基本使用
linux·c语言·microsoft
xiaomo22492 小时前
javaee-网络原理(理论)
linux·服务器·网络