Linux线程控制

一、互斥:临界资源的排他性访问

1. 核心概念

互斥,即对临界资源的排他性访问,是多线程安全的基础。

  • 临界资源:多线程环境下,会被多个线程同时读写的资源,比如全局变量、文件句柄、硬件设备等。这类资源的读写操作不具备原子性,直接并发访问会导致数据一致性问题。
  • 排他访问:同一时刻,只能有一个线程对临界资源进行读写操作,其他线程必须等待,直到当前线程释放资源。

2. 为什么需要互斥?

以一个简单的 A++ 操作为例,这个看似简单的语句,在汇编层面至少会被拆解为 3 步:

  1. 从内存中读取变量A的值到寄存器;
  2. 将寄存器中的值加 1;
  3. 将寄存器的值写回内存中的A

在多线程并发时,线程调度可能发生在任意步骤之间。比如线程th1执行完前两步后被切换,线程th2接着执行完整的三步,此时th1再切回继续执行第三步,就会覆盖th2的修改,最终导致数据错误。

互斥机制的作用,就是将这段非原子性的代码包裹为原子操作,确保其在一次线程调度中完整执行。

3. 互斥锁的使用步骤与核心 API

在 Linux 多线程编程中,互斥锁的核心数据结构是 pthread_mutex_t,使用流程遵循 定义→初始化→加锁→解锁→销毁 的五步原则,每个步骤都对应明确的函数接口。

(1)定义互斥锁
复制代码
#include <pthread.h>
// 定义一个互斥锁变量(全局变量保证所有线程可见)
pthread_mutex_t g_mutex;
(2)初始化互斥锁
复制代码
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • 功能:初始化已定义的互斥锁。
  • 参数
    • mutex:指向要初始化的互斥锁变量的指针;
    • attr:互斥锁属性,传入NULL表示使用默认属性。
  • 返回值 :成功返回0,失败返回非零错误码。
(3)加锁操作
复制代码
int pthread_mutex_lock(pthread_mutex_t *mutex);
  • 功能 :对临界区代码加锁,加锁后的代码至解锁前的区域为原子操作,不允许线程调度打断。
  • 关键特性 :如果互斥锁已被其他线程持有,当前线程会阻塞等待,直到锁被释放。
  • 参数mutex:指向已初始化的互斥锁指针。
  • 返回值 :成功返回0,失败返回非零错误码。
(4)解锁操作
复制代码
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 功能:释放持有的互斥锁,允许其他等待的线程获取锁并执行临界区代码。
  • 核心规则 :加锁和解锁必须由同一个线程执行,不允许跨线程解锁。
  • 参数mutex:指向已加锁的互斥锁指针。
  • 返回值 :成功返回0,失败返回非零错误码。
(5)销毁互斥锁
复制代码
int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 功能:互斥锁使用完毕后,释放其占用的系统资源。
  • 参数mutex:指向要销毁的互斥锁指针。
  • 返回值 :成功返回0,失败返回非零错误码。
(6)互斥锁核心使用规范
  • 临界区代码要短小精悍:加锁后的代码执行时间越长,线程阻塞等待的时间就越久,会严重降低程序的并发效率。
  • 临界区内禁止休眠 / 大耗时操作:在临界区中调用sleep()read()/write()(大文件)等耗时操作,会导致锁被长时间持有,其他线程无法执行,完全丧失并发优势。
(7)互斥锁完整代码示例
复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 1. 定义互斥锁
pthread_mutex_t g_mutex;
// 临界资源:全局计数器
int g_counter = 0;

// 线程函数:对计数器累加
void *thread_func(void *arg) {
    int thread_id = *(int *)arg;
    for (int i = 0; i < 5; i++) {
        // 3. 加锁:pthread_mutex_lock
        int lock_ret = pthread_mutex_lock(&g_mutex);
        if (lock_ret != 0) {
            printf("线程%d 加锁失败!错误码:%d\n", thread_id, lock_ret);
            continue;
        }

        // 临界区(原子操作,保证同一时刻只有一个线程执行)
        // 遵循"短小精悍"原则:仅保留核心的临界资源操作
        g_counter++;
        printf("线程%d 累加后:g_counter = %d\n", thread_id, g_counter);
        // 注意:此处若加sleep(1),会导致另一个线程长时间阻塞,违背互斥锁使用规范

        // 4. 解锁:必须由当前加锁线程执行
        int unlock_ret = pthread_mutex_unlock(&g_mutex);
        if (unlock_ret != 0) {
            printf("线程%d 解锁失败!错误码:%d\n", thread_id, unlock_ret);
        }
        sleep(1); // 非临界区可执行耗时操作
    }
    return NULL;
}

int main() {
    pthread_t th1, th2;
    int id1 = 1, id2 = 2;

    // 2. 初始化互斥锁(默认属性)
    int init_ret = pthread_mutex_init(&g_mutex, NULL);
    if (init_ret != 0) {
        printf("互斥锁初始化失败!错误码:%d\n", init_ret);
        return -1;
    }

    // 创建两个线程
    pthread_create(&th1, NULL, thread_func, &id1);
    pthread_create(&th2, NULL, thread_func, &id2);

    // 等待线程结束
    pthread_join(th1, NULL);
    pthread_join(th2, NULL);

    // 5. 销毁互斥锁
    int destroy_ret = pthread_mutex_destroy(&g_mutex);
    if (destroy_ret != 0) {
        printf("互斥锁销毁失败!错误码:%d\n", destroy_ret);
        return -1;
    }

    printf("最终计数器值:%d\n", g_counter);
    return 0;
}
(8)运行结果说明

两个线程会交替对 g_counter 进行累加,最终结果稳定为 10,不会出现数据不一致问题。如果去掉互斥锁,最终结果会小于 10,且每次运行结果都不相同。

二、同步:线程的有序化执行

1. 核心概念

同步有先后顺序的排他性资源访问,它要求线程按照预定的逻辑顺序执行,本质上是互斥的一个特例。

比如生产消费模型中,必须保证生产者线程生产出数据后,消费者线程才能读取数据,这就是典型的同步场景。

2. 同步与互斥的核心区别

特性 互斥锁 信号量(同步)
核心目标 排他性访问临界资源 按顺序访问临界资源
锁 / 资源释放方 加锁线程自己释放 由其他线程交叉释放(th1 释放 th2,th2 释放 th1)
临界区限制 禁止休眠、大耗时操作 允许短时间休眠、小耗时操作
资源数量 仅支持单一资源 支持多资源(计数信号量)

3. 信号量:实现同步的核心工具

在 Linux 中,同步机制的实现通常依赖信号量 ,其核心数据结构是 sem_t,使用流程为 定义→初始化→PV 操作→销毁

(1)定义信号量
复制代码
#include <semaphore.h>
// 定义一个信号量变量
sem_t g_sem;
(2)初始化信号量
复制代码
int sem_init(sem_t *sem, int pshared, unsigned int value);
  • 功能:初始化信号量,设置其共享属性和初始值。
  • 参数
    • sem:指向要初始化的信号量变量的指针;
    • pshared:共享属性,0表示线程间使用,非0表示进程间使用;
    • value:信号量初始值,0表示无资源(线程阻塞),1表示单资源(二值信号量),大于1表示多资源(计数信号量)。
  • 返回值 :成功返回0,失败返回-1
(3)信号量的 PV 操作

信号量的核心操作是P 操作(申请资源)V 操作(释放资源),这两个操作都是原子操作,对应两个核心函数。

P 操作:申请资源(sem_wait)
复制代码
int sem_wait(sem_t *sem);
  • 功能 :尝试申请信号量资源,执行 sem = sem - 1
    • 若操作后 sem >= 0,线程继续执行;
    • 若操作后 sem < 0,线程阻塞等待,直到有其他线程释放资源。
  • 参数sem:指向已初始化的信号量指针。
  • 返回值 :成功返回0,失败返回-1
V 操作:释放资源(sem_post)
复制代码
int sem_post(sem_t *sem);
  • 功能 :释放信号量资源,执行 sem = sem + 1
  • 核心规则:由目标线程的 "依赖线程" 交叉释放(如消费者释放生产者、生产者释放消费者)。
  • 关键特性:线程执行该函数时不会阻塞,释放后会唤醒等待该信号量的线程。
  • 参数sem:指向已初始化的信号量指针。
  • 返回值 :成功返回0,失败返回-1
(4)销毁信号量
复制代码
int sem_destroy(sem_t *sem);
  • 功能:释放信号量占用的系统资源。
  • 参数sem:指向要销毁的信号量指针。
  • 返回值 :成功返回0,失败返回-1
(5)计数信号量的特殊用法

信号量初值可设置为大于 1 的数值(如 3、5),适用于多资源互斥场景(资源数本身不唯一)。例如:

  • 初始化信号量 sem_init(&sem, 0, 3),表示同时允许 3 个线程访问临界资源;
  • 每个线程执行sem_wait()申请资源,sem_post()释放资源;
  • 当第 4 个线程执行sem_wait()时,会阻塞等待前 3 个线程中任意一个释放资源。
(6)信号量核心使用规范
  • 允许短时间休眠 / 小耗时操作:信号量的核心目标是保证线程执行顺序,而非极致的并发效率,因此临界区中可执行sleep(1)等短耗时操作;
  • 交叉释放规则:同步场景下,信号量的 PV 操作需由不同线程交叉执行(如生产者 V 操作释放消费者,消费者 V 操作释放生产者)。
(7)信号量完整代码示例(生产消费模型)
复制代码
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

// 定义信号量:控制生产消费顺序
sem_t g_sem_producer; // 生产者信号量
sem_t g_sem_consumer; // 消费者信号量
// 临界资源:产品缓冲区
int g_product = 0;

// 生产者线程函数
void *producer_func(void *arg) {
    for (int i = 0; i < 5; i++) {
        // P操作:申请生产者资源(初始值为1,可直接执行)
        sem_wait(&g_sem_producer);

        // 生产产品(允许短耗时操作)
        g_product = i + 1;
        printf("生产者生产产品:%d\n", g_product);
        sleep(1); // 模拟生产耗时(信号量场景下允许)

        // V操作:释放消费者资源(交叉释放,让消费者可以消费)
        sem_post(&g_sem_consumer);
    }
    return NULL;
}

// 消费者线程函数
void *consumer_func(void *arg) {
    for (int i = 0; i < 5; i++) {
        // P操作:申请消费者资源(初始值为0,阻塞等待生产者释放)
        sem_wait(&g_sem_consumer);

        // 消费产品(允许短耗时操作)
        printf("消费者消费产品:%d\n", g_product);
        sleep(1); // 模拟消费耗时(信号量场景下允许)

        // V操作:释放生产者资源(交叉释放,让生产者可以继续生产)
        sem_post(&g_sem_producer);
    }
    return NULL;
}

int main() {
    pthread_t th_producer, th_consumer;

    // 初始化信号量
    // 生产者信号量初始值1:允许先生产
    sem_init(&g_sem_producer, 0, 1);
    // 消费者信号量初始值0:必须等生产后才能消费
    sem_init(&g_sem_consumer, 0, 0);

    // 创建线程
    pthread_create(&th_producer, NULL, producer_func, NULL);
    pthread_create(&th_consumer, NULL, consumer_func, NULL);

    // 等待线程结束
    pthread_join(th_producer, NULL);
    pthread_join(th_consumer, NULL);

    // 销毁信号量
    sem_destroy(&g_sem_producer);
    sem_destroy(&g_sem_consumer);

    return 0;
}
(8)计数信号量示例(多资源访问)
复制代码
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

// 定义计数信号量:初始值3,允许3个线程同时访问
sem_t g_count_sem;
// 临界资源:资源使用计数
int g_res_used = 0;

void *thread_func(void *arg) {
    int thread_id = *(int *)arg;
    // P操作:申请资源
    sem_wait(&g_count_sem);
    
    g_res_used++;
    printf("线程%d 占用资源,当前使用数:%d\n", thread_id, g_res_used);
    sleep(2); // 模拟小耗时操作
    
    // 释放资源
    g_res_used--;
    printf("线程%d 释放资源,当前使用数:%d\n", thread_id, g_res_used);
    // V操作:释放资源
    sem_post(&g_count_sem);
    
    return NULL;
}

int main() {
    pthread_t th[5];
    int ids[5] = {1,2,3,4,5};
    
    // 初始化计数信号量:允许3个线程同时访问
    sem_init(&g_count_sem, 0, 3);
    
    // 创建5个线程
    for (int i = 0; i < 5; i++) {
        pthread_create(&th[i], NULL, thread_func, &ids[i]);
    }
    
    // 等待所有线程结束
    for (int i = 0; i < 5; i++) {
        pthread_join(th[i], NULL);
    }
    
    sem_destroy(&g_count_sem);
    return 0;
}
(9)运行结果说明
  • 生产消费模型:严格按照生产→消费→生产→消费的顺序执行;
  • 计数信号量模型:同一时刻最多有 3 个线程占用资源,第 4、5 个线程会阻塞,直到前 3 个线程释放资源。

三、死锁:多线程编程的 "隐形陷阱"

1. 死锁的概念

死锁是指由于锁资源的申请和释放逻辑不合理,导致多个线程互相等待对方持有的锁,最终所有线程都无法继续执行的现象。

比如线程 A 持有锁 1,等待锁 2;线程 B 持有锁 2,等待锁 1,此时两个线程会永远阻塞,程序陷入停滞。

2. 死锁产生的四个必要条件

死锁的发生必须同时满足以下四个条件,只要破坏其中任意一个,就能避免死锁。

  1. 互斥条件:一个资源每次只能被一个线程使用。
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源,在未使用完毕前,不能被强行剥夺。
  4. 循环等待条件:若干线程之间形成头尾相接的循环等待资源关系。

3. 死锁的规避思路

  • 固定顺序申请锁:所有线程都按照相同的顺序获取多个锁,避免循环等待。比如线程 A 和线程 B 都先获取锁 1,再获取锁 2。
  • 锁的申请时限:使用 pthread_mutex_trylock() 尝试加锁,设置超时时间,超时后放弃申请并释放已持有的锁。
  • 减少锁的嵌套:尽量避免一个临界区内部再申请其他锁,降低锁依赖的复杂度。
  • 资源一次性申请:在线程执行初期,一次性申请所有需要的锁,避免中途申请新锁。

四、总结

在 Linux 多线程编程中,互斥锁 解决了临界资源的排他性访问问题,保证了数据一致性,核心规则是 "加解锁同线程、临界区短小精悍";信号量在此基础上实现了线程的有序执行,核心规则是 "交叉释放资源、允许短耗时操作",计数信号量还可适配多资源互斥场景。而死锁作为多线程编程的常见问题,需要我们通过规范锁的使用逻辑来规避。

相关推荐
lingran__2 小时前
C语言字符函数和字符串函数详解
c语言·开发语言
zengxiaobai2 小时前
客户端 address 不足
linux
CodeCraft Studio2 小时前
JavaScript图表库 DHTMLX Diagram 6.1 重磅发布:全新PERT模式上线,项目可视化能力再升级!
开发语言·javascript·ecmascript·dhtmlx·图表开发·diagram·javascript图表库
Dxy12393102162 小时前
Python的正则表达式如何做数据校验
开发语言·python·正则表达式
UP_Continue2 小时前
C++--右值和移动语义
开发语言·c++
代码游侠2 小时前
学习笔记——线程控制 - 互斥与同步
linux·运维·笔记·学习·算法
牛奶咖啡133 小时前
Linux常见系统故障案例说明并修复解决(下)
linux·服务器·文件系统挂载异常分析并修复·持久化挂载分区文件丢失故障修复·分析系统进程cpu占用率过高
222you3 小时前
Java线程的三种创建方式
java·开发语言
云上漫步者3 小时前
深度实战:Rust交叉编译适配OpenHarmony PC——unicode_width完整适配案例
开发语言·后端·rust·harmonyos