POSIX信号量:原理、应用与二元信号量模拟互斥

目录

一、POSIX信号量的原理

1、临界资源保护的传统方式

2、临界资源的细分与并行访问

3、信号量的概念与作用

4、信号量的PV操作机制

P操作(申请信号量)

V操作(释放信号量)

5、PV操作的原子性要求

6、信号量的实现细节

7、申请信号量失败的处理

二、互斥锁、条件变量和信号量的区别与联系

1、核心区别

作用目标与粒度

操作机制

使用场景

2、内在联系

互补性

功能扩展性

3、典型应用场景对比

4、选择建议

三、POSIX信号量核心函数

1、初始化信号量 (sem_init)

2、销毁信号量 (sem_destroy)

3、等待信号量(申请信号量,也就是P操作) (sem_wait)

1. sem_wait —— 基本等待操作

函数原型

功能

返回值

典型用途

示例代码

注意事项

2. sem_trywait —— 非阻塞等待

函数原型

功能

返回值

典型用途

示例代码

注意事项

3. sem_timedwait —— 超时等待

函数原型

功能

返回值

典型用途

示例代码

注意事项

三者的对比总结

关键注意事项

4、发布信号量(释放信号量,也就是V操作) (sem_post)

5、POSIX信号量 vs System V信号量

6、完整生产者-消费者示例

四、二元信号量模拟实现互斥功能

1、信号量基本概念

二元信号量的特性

2、原始问题:多线程抢票系统

问题分析

输出结果

3、解决方案:使用二元信号量实现互斥

改进点说明

输出结果

信号量操作细节

4、执行流程分析

5、优势对比

6、扩展思考

五、资源同步机制选择指南:Semaphore vs Mutex

1、资源可拆分时:使用信号量(Semaphore)

2、资源整体使用时:使用互斥锁(Mutex)

3、关键对比与决策流程图

4、高级场景扩展

5、代码示例:混合使用Semaphore与Mutex

6、总结


一、POSIX信号量的原理

        在多线程或多进程的并发编程环境中,存在一种特殊的资源——临界资源。这类资源可能会被多个执行流(如线程或进程)同时访问,若缺乏有效的保护机制,极易引发数据不一致、竞态条件等严重问题。

1、临界资源保护的传统方式

  • 当我们采用单一的互斥锁对临界资源进行保护时,实际上是将整个临界资源视为一个不可分割的整体。

  • 在这种模式下,同一时刻仅允许一个执行流对临界资源进行访问。

  • 这种保护方式虽然简单直接,但在某些场景下却显得过于“粗暴”,因为它忽略了临界资源内部可能存在的细分结构。

2、临界资源的细分与并行访问

  • 实际上,我们可以将临界资源进一步细分为多个独立的区域或子资源。

  • 当多个执行流需要访问临界资源时,如果它们访问的是不同的区域或子资源,那么这些执行流完全可以同时进行访问,而不会引发数据不一致等问题。

  • 这种更细粒度的资源管理方式,能够显著提高系统的并发性能和资源利用率。

3、信号量的概念与作用

  • 为了实现上述更细粒度的资源管理,我们引入了信号量(也称为信号灯)的概念。

  • 信号量本质上是一个计数器,用于描述临界资源中可用的资源数目。

  • 通过信号量,我们可以对临界资源进行更为精细的管理和控制。

4、信号量的PV操作机制

P操作(申请信号量)

  • 当一个执行流需要访问临界资源时,它必须先申请信号量。这一过程被称为P操作。

  • P操作的本质是申请获得临界资源中某块特定资源的使用权限。

  • 如果申请成功,信号量中的计数器值将减一,表示可用资源数目减少。

  • 如果申请失败(即信号量值为0,表示所有资源已被占用),则执行流将被挂起,进入等待队列。

V操作(释放信号量)

  • 当一个执行流完成对临界资源的访问后,它必须释放信号量。这一过程被称为V操作。

  • V操作的本质是归还临界资源中某块特定资源的使用权限。

  • 如果释放成功,信号量中的计数器值将加一,表示可用资源数目增加。

  • 同时,系统会检查等待队列中是否有被挂起的执行流,如果有,则唤醒其中一个执行流,使其有机会申请信号量并访问临界资源。

5、PV操作的原子性要求

  • 由于多个执行流会竞争式地申请信号量,因此信号量本身也会成为被多个执行流同时访问的临界资源。

  • 然而,信号量本身就是用于保护临界资源的工具,我们不可能再用信号量去保护信号量本身。

  • 因此,信号量的PV操作必须是原子操作,即它们必须在一个不可分割的步骤中完成,以确保在任何时候都不会出现多个执行流同时修改信号量状态的情况。

6、信号量的实现细节

  • 计数器与等待队列:信号量不仅包含一个计数器用于记录可用资源数目,还包含一个等待队列用于挂起和唤醒执行流。

  • 原子操作的实现:由于内存中的变量++、--操作并不是原子操作,因此信号量的实现不能简单地依赖于对全局变量的++、--操作。在实际实现中,通常会使用硬件提供的原子指令(如CAS指令)或操作系统提供的同步机制(如锁、条件变量等)来确保PV操作的原子性。

7、申请信号量失败的处理

  • 当执行流在申请信号量时,如果发现信号量的值为0(即所有资源已被占用),则该执行流将被挂起并放入信号量的等待队列中。

  • 直到有执行流释放信号量并唤醒等待队列中的某个执行流时,被挂起的执行流才有机会重新申请信号量并访问临界资源。

        综上所述,POSIX信号量通过PV操作机制和原子性要求,实现了对临界资源的更细粒度管理和保护。它不仅提高了系统的并发性能和资源利用率,还确保了数据的一致性和完整性。


二、互斥锁、条件变量和信号量的区别与联系

在POSIX多线程编程中,互斥锁、条件变量和信号量均用于线程同步,但三者作用机制和应用场景存在显著差异,具体区别与联系如下:

1、核心区别

作用目标与粒度

  • 互斥锁:保护单一共享资源,确保同一时间仅一个线程访问临界区。其本质是二进制信号量(计数值为0或1),适用于严格互斥场景(如修改共享变量)。

  • 信号量:管理多个同类资源,通过计数器控制并发访问数量。计数值可大于1,允许多线程同时访问资源(如连接池管理)。

  • 条件变量:不直接控制资源访问,而是协调线程执行顺序。它允许线程在特定条件不满足时主动阻塞,待条件变化后被唤醒(如生产者-消费者模型中队列非空时唤醒消费者)。

操作机制

  • 互斥锁:通过lock()/unlock()实现加锁/解锁。若锁被占用,线程阻塞;解锁后唤醒一个等待线程。

  • 信号量:通过P操作(wait)/V操作(signal)修改计数器。P操作减1(资源不足则阻塞),V操作加1(唤醒一个等待线程)。

  • 条件变量:需与互斥锁配合使用。线程调用wait()时释放关联互斥锁并阻塞;其他线程通过notify_one()/notify_all()唤醒等待线程,被唤醒线程重新获取互斥锁后继续执行。

使用场景

  • 互斥锁:适用于需要严格互斥的场景,如修改共享数据结构、保护全局变量。

  • 信号量:适用于管理有限资源(如线程池、数据库连接池)或实现复杂同步逻辑(如限制并发任务数量)。

  • 条件变量:适用于线程间依赖状态变化的场景,如生产者-消费者模型、任务队列处理。

2、内在联系

互补性

  • 互斥锁与信号量可协同工作:信号量控制资源数量,互斥锁保护资源操作。例如,在连接池中,信号量限制最大连接数,互斥锁确保连接分配/释放的原子性。

  • 条件变量与互斥锁必须绑定使用:条件变量依赖互斥锁保护共享状态,避免竞态条件。例如,消费者线程在检查队列是否为空时需持有互斥锁。

功能扩展性

  • 信号量可模拟互斥锁:将信号量初始化为1,其P/V操作即等价于互斥锁的lock/unlock

  • 条件变量可实现部分信号量功能:通过条件判断和通知机制,可替代信号量解决某些同步问题(如线程按顺序执行),但需额外逻辑处理资源计数。

3、典型应用场景对比

机制生产者-消费者模型读者-写者问题任务调度
互斥锁保护共享队列的插入/删除操作,防止数据竞争。保护读者计数器,确保写操作独占访问。保护任务队列,防止并发修改。
信号量控制消费者线程数量(如缓冲区满时阻塞生产者)。限制最大读者数(如信号量计数值为N)。限制同时运行的任务数(如线程池)。
条件变量消费者线程在队列为空时阻塞,生产者线程在添加数据后唤醒消费者。写者线程在有读者时阻塞,读者线程在无写者时唤醒其他读者。任务线程在队列为空时阻塞,调度线程在添加任务后唤醒任务线程。

4、选择建议

  • 优先使用互斥锁:若需严格互斥访问单一资源,且无复杂同步需求(如修改共享变量)。

  • 选择信号量:若需管理多个同类资源或实现复杂同步逻辑(如限制并发连接数)。

  • 选用条件变量:若线程执行依赖共享状态变化,且需高效唤醒机制(如生产者-消费者模型)。

  • 组合使用:复杂场景中常需组合使用三者。例如,信号量控制资源数量,互斥锁保护资源操作,条件变量协调线程执行顺序。


三、POSIX信号量核心函数

        POSIX信号量提供了一套轻量级的线程/进程间同步机制,通过计数器管理共享资源访问权限。POSIX信号量和SystemV信号量具有相同的核心功能,都用于实现同步机制,确保对共享资源的无冲突访问。不同的是,POSIX信号量还支持线程间的同步操作。以下是其核心API的完整说明:

1、初始化信号量 (sem_init)

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);

参数说明

  • sem:指向待初始化的信号量对象的指针。

  • pshared

    • 0:信号量在线程间共享(同一进程内)。

    • 0:信号量在进程间共享(需通过共享内存分配sem)。

  • value:信号量的初始计数值(表示可用资源数量)。

返回值:成功返回0,失败返回-1并设置errno(如EINVAL参数无效)。

示例代码

#include <semaphore.h>
#include <stdio.h>

int main() {
    sem_t sem;
    if (sem_init(&sem, 0, 3) == -1) {  // 线程间共享,初始值3
        perror("sem_init failed");
        return 1;
    }
    printf("Semaphore initialized successfully.\n");
    sem_destroy(&sem);  // 清理资源
    return 0;
}

关键注意事项

  • 进程间共享时,sem需位于共享内存区域(如通过mmap分配)。

  • 未初始化的信号量或重复初始化会导致未定义行为。

2、销毁信号量 (sem_destroy)

int sem_destroy(sem_t *sem);

参数说明sem:待销毁的信号量对象指针。

返回值:成功返回0,失败返回-1(如信号量正被等待)。

示例代码

sem_t sem;
sem_init(&sem, 0, 1);
// ... 使用信号量 ...
if (sem_destroy(&sem) == -1) {
    perror("sem_destroy failed");
}

关键注意事项

  • 确保无线程正在等待该信号量,否则可能引发资源泄漏。

  • 进程间共享的信号量无需手动销毁(系统自动回收)。

3、等待信号量(申请信号量,也就是P操作) (sem_wait)

1. sem_wait —— 基本等待操作

函数原型
#include <semaphore.h>
int sem_wait(sem_t *sem);
功能
  • 阻塞等待:若信号量 sem 的计数值 > 0,则减1并立即返回;若 == 0,则阻塞调用线程,直到信号量变为正数。

  • 原子性:操作是原子的,避免竞态条件。

返回值
  • 成功返回 0,失败返回 -1 并设置 errno(如 EINTR 被信号中断)。

典型用途
  • 用于线程/进程间的互斥或同步,确保对共享资源的独占访问。

示例代码
sem_t sem;
sem_init(&sem, 0, 1);  // 初始值为1(二进制信号量)

void* thread_func(void* arg) {
    sem_wait(&sem);    // 申请信号量
    // 临界区操作(独占访问共享资源)
    sem_post(&sem);    // 释放信号量
    return NULL;
}
注意事项
  • 阻塞风险:若信号量始终为0,线程可能无限期阻塞。

  • 中断处理:需处理中断异常(EINTR),也就是需检查 errno == EINTR 并处理信号中断(通常通过循环重试)。

    while (sem_wait(&sem) == -1 && errno == EINTR);

2. sem_trywait —— 非阻塞等待

函数原型
int sem_trywait(sem_t *sem);
功能
  • 非阻塞尝试:若信号量 sem 的计数值 > 0,则减1并返回;否则立即返回错误(EAGAIN)。

  • 避免阻塞:适用于不希望线程被挂起的场景。

返回值
  • 成功返回 0,失败返回 -1 并设置 errno

    • EAGAIN:信号量值为0(资源不可用)。

    • EINTR:操作被信号中断。

典型用途
  • 轮询检查资源可用性,或结合超时机制实现灵活同步。

示例代码
while (1) {
    if (sem_trywait(&sem) == 0) {
        // 成功获取信号量,执行临界区操作
        sem_post(&sem);
        break;
    } else if (errno == EAGAIN) {
        printf("Resource busy, retrying...\n");
        sleep(1);  // 等待后重试
    } else {
        perror("sem_trywait failed");
        break;
    }
}
注意事项
  • 需明确处理 EAGAIN 错误,避免误判为其他故障。

3. sem_timedwait —— 超时等待

函数原型
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
功能
  • 超时阻塞:在 abs_timeout 指定的绝对时间前等待信号量。若超时仍不可用,返回错误。

  • 时间参数abs_timeout 是 struct timespec 类型,表示绝对时间(如 time(NULL) + 5 表示5秒后超时)。

返回值
  • 成功返回 0,失败返回 -1 并设置 errno

    • ETIMEDOUT:超时未获取信号量。

    • EINTR:被信号中断。

典型用途
  • 避免无限阻塞,同时比轮询(sem_trywait)更高效。

示例代码
#include <time.h>

struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 3;  // 设置3秒后超时

if (sem_timedwait(&sem, &ts) == -1) {
    if (errno == ETIMEDOUT) {
        printf("Timeout reached!\n");
    } else {
        perror("sem_timedwait failed");
    }
} else {
    // 成功获取信号量
    sem_post(&sem);
}
注意事项
  • 时间类型:必须使用绝对时间(CLOCK_REALTIME),而非相对时间。

  • 精度:受系统时钟和调度影响,实际超时可能有微小偏差。

三者的对比总结

函数阻塞行为适用场景
sem_wait无限阻塞确定资源最终可用时使用
sem_trywait非阻塞轮询或快速失败逻辑
sem_timedwait超时阻塞需要避免死锁或响应超时

关键注意事项

  • 错误处理:始终检查返回值并处理 errno(如 EINTR)。

  • 信号量初始化:确保 sem_init 已正确调用,且 value 合理(如互斥锁初始为1)。

  • 释放信号量:配对使用 sem_post,避免死锁。

  • 线程安全:POSIX信号量本身是线程安全的,但共享资源的访问仍需额外保护。

4、发布信号量(释放信号量,也就是V操作) (sem_post)

int sem_post(sem_t *sem);

行为:将信号量计数值加1,并唤醒一个等待线程(若有)。

返回值:成功返回0,失败返回-1(如计数值溢出)。

示例代码

sem_t sem;
sem_init(&sem, 0, 0);  // 初始无资源

void* producer(void* arg) {
    // ... 生产资源 ...
    if (sem_post(&sem) == -1) {
        perror("sem_post failed");
    }
    printf("Resource available.\n");
    return NULL;
}

关键注意事项

  • 确保计数值不会溢出(unsigned int范围限制)。

  • sem_wait配对使用,避免逻辑错误。

5、POSIX信号量 vs System V信号量

特性POSIX信号量System V信号量
作用域线程间/进程间进程间(需通过键值访问)
性能更高(轻量级)较低(涉及内核操作)
API简洁性更直观(sem_wait/sem_post复杂(需semget/semop等)
命名支持无名信号量(内存中)支持命名信号量(文件系统节点)

6、完整生产者-消费者示例

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

#define BUFFER_SIZE 5

sem_t empty, full;
int buffer[BUFFER_SIZE];
int count = 0;

void* producer(void* arg) {
    for (int i = 0; i < 10; i++) {
        sem_wait(&empty);  // 等待空槽
        buffer[count] = i;
        count++;
        printf("Produced: %d\n", i);
        sem_post(&full);   // 通知消费者
    }
    return NULL;
}

void* consumer(void* arg) {
    for (int i = 0; i < 10; i++) {
        sem_wait(&full);   // 等待数据
        int item = buffer[--count];
        printf("Consumed: %d\n", item);
        sem_post(&empty);  // 释放空槽
    }
    return NULL;
}

int main() {
    pthread_t prod_thread, cons_thread;
    sem_init(&empty, 0, BUFFER_SIZE);  // 初始空槽数
    sem_init(&full, 0, 0);             // 初始数据数

    pthread_create(&prod_thread, NULL, producer, NULL);
    pthread_create(&cons_thread, NULL, consumer, NULL);

    pthread_join(prod_thread, NULL);
    pthread_join(cons_thread, NULL);

    sem_destroy(&empty);
    sem_destroy(&full);
    return 0;
}

        通过上述内容,我们可以全面掌握POSIX信号量的使用方法,包括初始化、销毁、等待和发布操作,以及与System V信号量的对比和典型应用场景。


四、二元信号量模拟实现互斥功能

1、信号量基本概念

        信号量(Semaphore)本质上是一个计数器,用于多线程环境中控制对共享资源的访问。当信号量的初始值设置为1时,这种特殊的信号量被称为二元信号量或二进制信号量。

二元信号量的特性

  • 初始值为1,表示资源可用

  • 每次只能有一个线程获取信号量(P操作)

  • 线程使用完资源后释放信号量(V操作)

  • 相当于一种特殊的互斥锁(mutex)

2、原始问题:多线程抢票系统

下面是一个未加保护的多线程抢票系统实现,存在数据竞争问题:

        在主线程中创建四个新线程来执行抢票逻辑。这些线程共享一个全局变量tickets来记录剩余票数,该变量作为临界资源会被多个线程同时访问。每次抢票操作后,程序都会输出当前剩余票数。需要注意的是,以下代码示例中并未对tickets变量采取任何保护措施。

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>

int tickets = 50;

void* TicketGrabbing(void* arg) {
    std::string name = (char*)arg;
    while (true) {
        if (tickets > 0) {
            usleep(1000); // 模拟处理延迟
            std::cout << name << " get a ticket, tickets left: " << --tickets << std::endl;
        }
        else {
            break;
        }
    }
    std::cout << name << " quit..." << std::endl;
    pthread_exit((void*)0);
}

int main() {
    pthread_t tid1, tid2, tid3, tid4;
    pthread_create(&tid1, nullptr, TicketGrabbing, (void*)"thread 1");
    pthread_create(&tid2, nullptr, TicketGrabbing, (void*)"thread 2");
    pthread_create(&tid3, nullptr, TicketGrabbing, (void*)"thread 3");
    pthread_create(&tid4, nullptr, TicketGrabbing, (void*)"thread 4");
    
    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);
    pthread_join(tid4, nullptr);
    return 0;
}

问题分析

  1. 数据竞争:多个线程同时访问和修改全局变量tickets

  2. 检查再行动(Check-Then-Act)问题

    • 线程检查tickets > 0

    • 在执行--tickets之前,其他线程可能已经修改了tickets的值

  3. 可能出现的错误

    • 票数被减到负数

    • 打印输出不一致

输出结果

3、解决方案:使用二元信号量实现互斥

        下面是通过二元信号量保护共享资源的改进实现:让每个线程在访问全局变量tickets之前先申请信号量,访问完毕后再释放信号量,此时二元信号量就达到了互斥的效果。

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

// 信号量封装类
class Sem {
public:
    Sem(int num) {
        sem_init(&_sem, 0, num); // 初始化信号量,0表示线程间共享,num为初始值
    }
    ~Sem() {
        sem_destroy(&_sem); // 销毁信号量
    }
    void P() { // 获取信号量(等待)
        sem_wait(&_sem);
    }
    void V() { // 释放信号量(发布)
        sem_post(&_sem);
    }
private:
    sem_t _sem;
};

// 全局二元信号量,初始值为1
Sem sem(1); 
int tickets = 50;

void* TicketGrabbing(void* arg) {
    std::string name = (char*)arg;
    while (true) {
        sem.P(); // 进入临界区前获取信号量
        
        // 临界区开始
        if (tickets > 0) {
            usleep(1000); // 模拟处理延迟
            std::cout << name << " get a ticket, tickets left: " << --tickets << std::endl;
            sem.V(); // 离开临界区前释放信号量
        }
        else {
            sem.V(); // 确保信号量被释放
            break;
        }
        // 临界区结束
    }
    std::cout << name << " quit..." << std::endl;
    pthread_exit((void*)0);
}

int main() {
    pthread_t tid1, tid2, tid3, tid4;
    pthread_create(&tid1, nullptr, TicketGrabbing, (void*)"thread 1");
    pthread_create(&tid2, nullptr, TicketGrabbing, (void*)"thread 2");
    pthread_create(&tid3, nullptr, TicketGrabbing, (void*)"thread 3");
    pthread_create(&tid4, nullptr, TicketGrabbing, (void*)"thread 4");
    
    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);
    pthread_join(tid4, nullptr);
    return 0;
}

改进点说明

信号量封装类

  • 封装了POSIX信号量操作,提供更面向对象的接口

  • P()操作对应sem_wait(),用于获取信号量

  • V()操作对应sem_post(),用于释放信号量

临界区保护

  • 所有对共享变量tickets的访问都放在sem.P()sem.V()之间

  • 确保同一时间只有一个线程能访问临界资源

信号量释放

  • 即使在tickets <= 0的情况下也确保释放信号量

  • 避免死锁情况

输出结果

运行代码后,剩余票数不会出现负数情况。这是因为同一时间仅有一个执行流能访问全局变量tickets,从而确保了数据的一致性。

信号量操作细节

sem_init()

  • 第一个参数:信号量指针

  • 第二个参数:0表示线程间共享,非0表示进程间共享

  • 第三个参数:信号量初始值

sem_wait() (P操作)

  • 如果信号量值>0,则减1并继续

  • 如果信号量值=0,则阻塞直到信号量值>0

sem_post() (V操作)

  • 信号量值加1

  • 如果有线程在等待,唤醒其中一个

4、执行流程分析

  1. 主线程创建4个抢票线程

  2. 每个线程执行TicketGrabbing函数:

    • 尝试获取信号量(P操作)

    • 如果成功获取:

      • 检查是否有余票

      • 如果有票,模拟处理延迟后减少票数

      • 释放信号量(V操作)

      • 如果没有票,释放信号量后退出循环

  3. 主线程等待所有子线程结束

5、优势对比

特性未加保护版本信号量保护版本
数据一致性可能不一致始终一致
票数负值可能出现不会出现
线程安全不安全安全
性能高(无锁)较低(有锁)
适用场景单线程或只读多线程读写

6、扩展思考

信号量与互斥锁的区别:(重点!!!)

  • 信号量可以用于线程同步(多个资源)和互斥(单个资源)

  • 互斥锁专门用于互斥,通常带有所有权概念(只有锁的持有者才能释放)

更复杂的同步场景

  • 可以使用计数信号量(初始值>1)控制对多个相同资源的访问

  • 可以实现生产者-消费者模型等复杂同步模式

性能考虑

  • 临界区应尽可能小,只包含必要的共享资源访问

  • 长时间的临界区会降低并发性能

通过这个例子,我们可以看到二元信号量如何有效地将竞态条件下的共享资源访问转化为受控的顺序访问,从而保证数据的一致性和程序的正确性。


五、资源同步机制选择指南:Semaphore vs Mutex

在多线程编程中,选择合适的同步机制(Semaphore或Mutex)取决于资源的可拆分性访问模式。以下是详细的技术对比和决策依据:

1、资源可拆分时:使用信号量(Semaphore)

适用场景:当资源可以被多个线程部分共享分段访问时(例如:连接池、线程池、文件读写缓冲区等)。

核心特性

  • 计数器机制:通过初始值表示可用资源数量,每次wait()减少计数,signal()增加计数。

  • 多线程并发访问:允许N个线程同时访问资源(N为信号量初始值)。

  • 灵活的资源控制:可动态调整资源可用量(通过signal()释放额外资源)。

典型用例

#include <semaphore.h>

sem_t pool_sem;
// 初始化信号量:允许3个线程同时访问资源
sem_init(&pool_sem, 0, 3);

void worker_thread() {
    sem_wait(&pool_sem);  // 获取资源(计数器-1)
    // 访问共享资源(如数据库连接)
    sem_post(&pool_sem);  // 释放资源(计数器+1)
}

优势

  • 高并发性:适合资源可拆分的场景,避免线程因竞争而阻塞。

  • 动态调整:可通过sem_post()在运行时增加可用资源。

注意事项

  • 需确保sem_wait()sem_post()成对调用,避免资源泄漏。

  • 信号量不保证线程访问顺序(FIFO或其他公平性)。

2、资源整体使用时:使用互斥锁(Mutex)

适用场景:当资源必须被独占访问时(例如:修改全局变量、写入文件、更新数据结构等)。

核心特性

  • 二进制状态:锁空闲(unlocked)或锁占用(locked)。

  • 互斥访问:同一时间仅允许一个线程持有锁。

  • 优先级保护:可结合条件变量(Condition Variable)实现更复杂的同步逻辑。

典型用例

#include <pthread.h>

pthread_mutex_t queue_mutex = PTHREAD_MUTEX_INITIALIZER;

void enqueue(int value) {
    pthread_mutex_lock(&queue_mutex);  // 加锁
    // 修改共享队列(如插入元素)
    pthread_mutex_unlock(&queue_mutex); // 解锁
}

优势

  • 强一致性:确保临界区操作的原子性。

  • 简单可靠:适合保护短时间操作的共享资源。

注意事项

  • 死锁风险:需避免嵌套加锁或未释放锁的情况。

  • 性能开销:频繁加锁/解锁可能影响性能(可通过锁粒度优化)。

3、关键对比与决策流程图

特性SemaphoreMutex
资源访问方式可部分共享(计数器控制)完全独占(二进制状态)
并发线程数支持多个线程同时访问仅允许一个线程访问
典型场景连接池、线程池、读写缓冲区修改全局变量、写入文件
公平性无保证无保证(可通过条件变量优化)
动态调整支持(sem_post()增加资源)不支持(需重新初始化)

决策流程

资源是否可拆分?

  • ❌ 否 → 使用Mutex(独占访问)。

  • ✅ 是 → 进入下一步。

需要限制并发访问数量吗?

  • ✅ 是 → 使用Semaphore(设置初始计数值)。

  • ❌ 否 → 考虑其他机制(如读写锁RWLock)。

4、高级场景扩展

  • 读写锁(RWLock):当资源读操作频繁但写操作稀疏时,可使用读写锁(如pthread_rwlock_t),允许多个读线程或单个写线程并发访问。

  • 条件变量(Condition Variable):与Mutex配合使用,实现线程间通知机制(如生产者-消费者模型)。

  • 原子操作(Atomic):对简单变量(如计数器)的修改,可使用C++11的std::atomic避免锁开销。

5、代码示例:混合使用Semaphore与Mutex

#include <semaphore.h>
#include <pthread.h>

sem_t resource_sem;  // 控制资源访问数量
pthread_mutex_t log_mutex;  // 保护日志写入

void process_resource(int id) {
    sem_wait(&resource_sem);  // 获取资源访问权限
    
    // 模拟资源访问
    pthread_mutex_lock(&log_mutex);
    printf("Thread %d is using resource\n", id);
    pthread_mutex_unlock(&log_mutex);
    
    sem_post(&resource_sem);  // 释放资源
}

int main() {
    sem_init(&resource_sem, 0, 2);  // 允许2个线程同时访问
    pthread_mutex_init(&log_mutex, NULL);
    
    // 创建多个线程...
    return 0;
}

6、总结

  • Semaphore:适合可拆分资源,通过计数器控制并发访问量。

  • Mutex:适合独占资源,确保临界区操作的原子性。

  • 根据实际场景选择同步机制,必要时可组合使用(如Semaphore+Mutex+条件变量)。

  • 始终遵循最小权限原则:锁的粒度越小,性能影响越低。

通过合理选择同步机制,可以显著提升多线程程序的并发性能和可靠性。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值