公司动态
C高级:关于互斥锁(Mutex)与信号量(Semaphore)的学习记录
多线程编程中多个线程同时访问共享资源时会产生数据竞争Data Race导致程序行为不可预测。互斥锁和信号量是 Linux 下最常用的两种同步机制用于保护共享资源、协调线程执行顺序。本文将从概念原理到实际代码系统讲解互斥锁和信号量的含义、作用以及如何创建和使用它们。以下是我在职坐标的学习记录一、互斥锁Mutex1.1 什么是互斥锁互斥锁Mutual Exclusion Lock简称 Mutex是一种用于保护共享资源的同步机制。它的核心思想是同一时刻只允许一个线程持有锁其他试图获取同一把锁的线程会被阻塞挂起等待直到锁被释放。可以这样理解互斥锁就像公共厕所的门锁——一个人进去后锁上门其他人只能在外面排队等待当前使用者出来并解锁后下一个人才能进去。1.2 互斥锁的作用防止数据竞争当多个线程同时读写同一个全局变量或共享资源时使用互斥锁可以保证同一时刻只有一个线程在操作避免数据不一致。保护临界区Critical Section临界区是指访问共享资源的那段代码。互斥锁确保临界区内的代码被原子地执行不会被其他线程打断。保证操作的原子性某些操作如count在 CPU 层面其实不是原子的包含读-改-写三步互斥锁可以让这类操作变得逻辑上原子。1.3 互斥锁的工作原理线程 A 线程 B │ │ ├─ pthread_mutex_lock() ──→ 获取锁成功 │ 进入临界区 │ │ ├─ pthread_mutex_lock() ──→ 阻塞等待... │ │ ├─ pthread_mutex_unlock() ──→ 释放锁 │ │ │ └─ 被唤醒获取锁成功 │ 进入临界区1.4 互斥锁的类型POSIX 标准定义了四种互斥锁类型类型说明PTHREAD_MUTEX_NORMAL普通锁默认。一个线程重复加锁会导致死锁。PTHREAD_MUTEX_ERRORCHECK检错锁。重复加锁会返回EDEADLK错误不会死锁。PTHREAD_MUTEX_RECURSIVE递归锁。允许同一线程多次加锁需要相同次数的解锁。PTHREAD_MUTEX_DEFAULT默认类型行为取决于系统实现通常是 NORMAL。二、互斥锁的创建与使用2.1 相关头文件#include pthread.h2.2 初始化互斥锁互斥锁可以通过两种方式初始化静态初始化和动态初始化。方式一静态初始化最简单pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER;适用于全局变量或静态变量使用默认属性PTHREAD_MUTEX_DEFAULT类型。方式二动态初始化可自定义属性#include pthread.h int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);参数类型说明mutexpthread_mutex_t *指向互斥锁变量的指针。attrconst pthread_mutexattr_t *互斥锁属性对象。传NULL使用默认属性。如果需要设置递归锁等特殊类型需要先初始化属性对象。动态初始化示例pthread_mutex_t mutex; // 使用默认属性 pthread_mutex_init(mutex, NULL); // 或者设置递归锁 pthread_mutexattr_t attr; pthread_mutexattr_init(attr); pthread_mutexattr_settype(attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init(mutex, attr); pthread_mutexattr_destroy(attr);2.3 加锁int pthread_mutex_lock(pthread_mutex_t *mutex);参数类型说明mutexpthread_mutex_t *要加锁的互斥锁指针。成功返回0当前线程持有锁可以进入临界区。失败返回错误码。如果锁已被其他线程持有调用线程会阻塞直到锁被释放。非阻塞加锁版本int pthread_mutex_trylock(pthread_mutex_t *mutex);如果锁已被占用不会阻塞而是立即返回EBUSY。适用于不想等待的场景。2.4 解锁int pthread_mutex_unlock(pthread_mutex_t *mutex);参数类型说明mutexpthread_mutex_t *要解锁的互斥锁指针。必须是当前线程持有的锁。成功返回0释放锁唤醒一个等待该锁的线程。失败返回错误码。常见错误包括对未持有的锁调用 unlockEPERM。2.5 销毁互斥锁int pthread_mutex_destroy(pthread_mutex_t *mutex);参数类型说明mutexpthread_mutex_t *要销毁的互斥锁指针。销毁前必须确保没有线程持有或等待该锁。静态初始化的互斥锁不需要显式销毁程序结束时自动释放。动态初始化的互斥锁使用完毕后应该调用destroy释放资源。2.6 完整示例多线程计数器以下示例展示不使用互斥锁和使用互斥锁的区别不加锁有数据竞争#include stdio.h #include pthread.h #define NUM_THREADS 4 #define INCREMENT_PER_THREAD 1000000 int counter 0; // 共享全局变量 void *increment(void *arg) { for (int i 0; i INCREMENT_PER_THREAD; i) { counter; // 非原子操作多个线程同时执行会丢失更新 } return NULL; } int main() { pthread_t threads[NUM_THREADS]; for (int i 0; i NUM_THREADS; i) { pthread_create(threads[i], NULL, increment, NULL); } for (int i 0; i NUM_THREADS; i) { pthread_join(threads[i], NULL); } // 期望值4,000,000 实际值通常小于期望值数据竞争 printf(counter %d (期望: %d)\n, counter, NUM_THREADS * INCREMENT_PER_THREAD); return 0; }加锁正确#include stdio.h #include pthread.h #define NUM_THREADS 4 #define INCREMENT_PER_THREAD 1000000 int counter 0; pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; // 静态初始化 void *increment(void *arg) { for (int i 0; i INCREMENT_PER_THREAD; i) { pthread_mutex_lock(mutex); // 加锁 counter; // 临界区 pthread_mutex_unlock(mutex); // 解锁 } return NULL; } int main() { pthread_t threads[NUM_THREADS]; for (int i 0; i NUM_THREADS; i) { pthread_create(threads[i], NULL, increment, NULL); } for (int i 0; i NUM_THREADS; i) { pthread_join(threads[i], NULL); } printf(counter %d (期望: %d)\n, counter, NUM_THREADS * INCREMENT_PER_THREAD); pthread_mutex_destroy(mutex); // 销毁互斥锁 return 0; }三、信号量Semaphore3.1 什么是信号量信号量Semaphore是一种更通用的同步机制它维护一个整型计数器用于控制同时访问某个资源的线程数量。与互斥锁不同信号量允许多个线程同时持有只要计数器大于 0因此它不仅可以用于互斥还可以用于协调线程的执行顺序如生产者-消费者模型。可以这样理解信号量就像一个停车场的剩余车位计数器——每进来一辆车计数器减 1P 操作每出去一辆车计数器加 1V 操作。当计数器为 0 时新的车只能在外面等待。3.2 信号量的作用互斥访问当信号量的初始值为 1 时它退化为一个互斥锁二值信号量用于保护共享资源。资源计数当信号量的初始值为 N 时它表示有 N 个可用资源。线程获取资源时计数器减 1释放资源时计数器加 1。同步协调信号量可以用于协调线程之间的执行顺序。例如生产者生产一个物品后对信号量加 1消费者在信号量大于 0 时才能消费。3.3 信号量的两种操作信号量只有两种原子操作操作名称效果说明P 操作Proberen荷兰语尝试sem_wait()信号量值减 1如果减 1 后值小于 0则线程阻塞等待V 操作Verhogen荷兰语增加sem_post()信号量值加 1如果有线程在等待唤醒其中一个初始值 3表示有 3 个资源 线程 A: sem_wait() → 值变为 2 → 成功获取资源 线程 B: sem_wait() → 值变为 1 → 成功获取资源 线程 C: sem_wait() → 值变为 0 → 成功获取资源 线程 D: sem_wait() → 值变为 -1 → 阻塞等待... 线程 A: sem_post() → 值变为 0 → 唤醒线程 D 线程 D: 被唤醒继续执行3.4 信号量 vs 互斥锁对比维度互斥锁Mutex信号量Semaphore本质二值状态锁定/解锁整型计数器同时访问只允许 1 个线程允许 N 个线程N 信号量初始值所有权有——只有加锁的线程才能解锁无——任何线程都可以 post用途保护共享资源互斥互斥 同步协调性能通常更快更简单稍慢涉及计数器操作头文件pthread.hsemaphore.h四、信号量的创建与使用4.1 相关头文件#include semaphore.h // 编译时链接 -lpthreadsem_open 可能需要 -lrt4.2 初始化信号量#include semaphore.h int sem_init(sem_t *sem, int pshared, unsigned int value);参数类型说明semsem_t *指向信号量变量的指针。psharedint控制信号量的共享范围。0表示信号量在同一进程的线程间共享最常用非 0 表示在多个进程间共享需要配合共享内存使用。valueunsigned int信号量的初始值。这个值决定了同时可以有多少个线程通过sem_wait()而不被阻塞。常见初始值sem_t sem; // 初始值为 1等价于互斥锁二值信号量 sem_init(sem, 0, 1); // 初始值为 5表示有 5 个可用资源 sem_init(sem, 0, 5); // 初始值为 0用于线程同步初始时阻塞 sem_init(sem, 0, 0);4.3 P 操作等待/获取int sem_wait(sem_t *sem);参数类型说明semsem_t *要操作的信号量指针。成功返回0信号量值减 1。如果减 1 后值仍 ≥ 0立即返回如果 0线程阻塞。失败返回-1并设置errno如EINTR被信号中断。非阻塞版本int sem_trywait(sem_t *sem);如果信号量值为 0不会阻塞而是立即返回-1并设置errno EAGAIN。带超时的版本Linuxint sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);最多等待到abs_timeout指定的绝对时间。超时后返回-1并设置errno ETIMEDOUT。4.4 V 操作释放/增加int sem_post(sem_t *sem);参数类型说明semsem_t *要操作的信号量指针。成功返回0信号量值加 1。如果有线程在等待该信号量唤醒其中一个。失败返回-1并设置errno。4.5 获取信号量当前值int sem_getvalue(sem_t *sem, int *sval);参数类型说明semsem_t *信号量指针。svalint *输出参数接收信号量的当前值。如果值为负数其绝对值表示当前等待的线程数。4.6 销毁信号量int sem_destroy(sem_t *sem);参数类型说明semsem_t *要销毁的信号量指针。销毁前必须确保没有线程在等待该信号量。五、信号量的完整示例5.1 示例一用信号量实现互斥二值信号量#include stdio.h #include pthread.h #include semaphore.h #define NUM_THREADS 4 #define INCREMENT_PER_THREAD 1000000 int counter 0; sem_t sem; // 二值信号量初始值为 1 void *increment(void *arg) { for (int i 0; i INCREMENT_PER_THREAD; i) { sem_wait(sem); // P 操作加锁 counter; sem_post(sem); // V 操作解锁 } return NULL; } int main() { pthread_t threads[NUM_THREADS]; sem_init(sem, 0, 1); // 初始值为 1 for (int i 0; i NUM_THREADS; i) { pthread_create(threads[i], NULL, increment, NULL); } for (int i 0; i NUM_THREADS; i) { pthread_join(threads[i], NULL); } printf(counter %d (期望: %d)\n, counter, NUM_THREADS * INCREMENT_PER_THREAD); sem_destroy(sem); return 0; }5.2 示例二生产者-消费者模型这是一个经典的使用场景生产者生产物品放入缓冲区消费者从缓冲区取出物品消费。使用两个信号量empty表示缓冲区中空闲位置的数量初始值为缓冲区大小。full表示缓冲区中已有物品的数量初始值为 0。#include stdio.h #include pthread.h #include semaphore.h #include unistd.h #define BUFFER_SIZE 5 #define NUM_ITEMS 10 int buffer[BUFFER_SIZE]; int in 0; // 生产者的写入位置 int out 0; // 消费者的读取位置 sem_t empty; // 空闲槽位数 sem_t full; // 已填充的槽位数 pthread_mutex_t mutex; // 保护 buffer 的互斥锁 void *producer(void *arg) { int id *(int *)arg; for (int i 0; i NUM_ITEMS; i) { int item id * 100 i; sem_wait(empty); // 等待空闲槽位 pthread_mutex_lock(mutex); buffer[in] item; printf([生产者 %d] 生产物品: %d, 放入位置 %d\n, id, item, in); in (in 1) % BUFFER_SIZE; pthread_mutex_unlock(mutex); sem_post(full); // 通知有新物品 usleep(100000); // 模拟生产时间 } return NULL; } void *consumer(void *arg) { int id *(int *)arg; for (int i 0; i NUM_ITEMS; i) { sem_wait(full); // 等待有物品可消费 pthread_mutex_lock(mutex); int item buffer[out]; printf([消费者 %d] 消费物品: %d, 从位置 %d\n, id, item, out); out (out 1) % BUFFER_SIZE; pthread_mutex_unlock(mutex); sem_post(empty); // 释放一个槽位 usleep(200000); // 模拟消费时间比生产慢 } return NULL; } int main() { pthread_t prod[2], cons[2]; int prod_ids[2] {1, 2}; int cons_ids[2] {1, 2}; sem_init(empty, 0, BUFFER_SIZE); // 初始有 5 个空位 sem_init(full, 0, 0); // 初始没有物品 pthread_mutex_init(mutex, NULL); // 创建 2 个生产者线程 for (int i 0; i 2; i) { pthread_create(prod[i], NULL, producer, prod_ids[i]); } // 创建 2 个消费者线程 for (int i 0; i 2; i) { pthread_create(cons[i], NULL, consumer, cons_ids[i]); } // 等待所有线程结束 for (int i 0; i 2; i) { pthread_join(prod[i], NULL); pthread_join(cons[i], NULL); } sem_destroy(empty); sem_destroy(full); pthread_mutex_destroy(mutex); printf(所有生产消费完成\n); return 0; }5.3 示例三用信号量实现线程同步控制执行顺序假设线程 B 必须等待线程 A 完成某项初始化工作后才能开始执行#include stdio.h #include pthread.h #include semaphore.h #include unistd.h sem_t sem; void *thread_a(void *arg) { printf([线程 A] 正在执行初始化工作...\n); sleep(2); // 模拟耗时操作 printf([线程 A] 初始化完成通知线程 B 可以开始\n); sem_post(sem); // V 操作通知线程 B return NULL; } void *thread_b(void *arg) { printf([线程 B] 等待线程 A 完成初始化...\n); sem_wait(sem); // P 操作阻塞等待 printf([线程 B] 收到通知开始执行任务\n); return NULL; } int main() { pthread_t tid_a, tid_b; sem_init(sem, 0, 0); // 初始值为 0线程 B 会阻塞 // 先创建线程 B它会阻塞等待 pthread_create(tid_b, NULL, thread_b, NULL); // 再创建线程 A pthread_create(tid_a, NULL, thread_a, NULL); pthread_join(tid_a, NULL); pthread_join(tid_b, NULL); sem_destroy(sem); return 0; }六、互斥锁与信号量的选型建议场景推荐方案保护共享变量如计数器、链表互斥锁保护复杂数据结构如数据库连接池互斥锁控制同时访问某资源的线程数量如连接池限流信号量初始值 最大并发数生产者-消费者模型信号量 互斥锁线程间的执行顺序同步信号量初始值为 0简单互斥不需要协调顺序互斥锁性能更好经验法则互斥锁用于互斥——保证同一时刻只有一个线程访问共享资源。信号量用于同步——协调多个线程的执行顺序或控制并发数量。实际项目中两者经常配合使用信号量控制并发数量互斥锁保护共享数据结构。七、常见陷阱与注意事项7.1 互斥锁的常见错误忘记解锁在临界区内return或break时忘记 unlock导致死锁。重复加锁对普通互斥锁NORMAL类型重复加锁会导致死锁。如果需要递归加锁使用PTHREAD_MUTEX_RECURSIVE类型。解锁未持有的锁对不是当前线程持有的锁调用 unlock 会导致未定义行为。7.2 信号量的常见错误初始值设置错误初始值决定了并发访问的数量设置不当会导致资源过度竞争或线程永久阻塞。忘记 destroy使用完毕后应调用sem_destroy()否则可能导致资源泄漏。在信号量等待时被信号中断sem_wait()可能被信号中断返回EINTR需要处理这种情况通常用循环重试。7.3 死锁的预防死锁是指两个或多个线程互相等待对方释放资源导致所有线程都无法继续执行。常见的预防策略包括锁排序如果多个线程需要获取多把锁始终按照相同的顺序获取如先锁 A 再锁 B。超时机制使用pthread_mutex_trylock()或sem_timedwait()获取失败时放弃并重试。最小化临界区尽量减少临界区内的代码量降低锁竞争的概率。八、总结机制核心作用初始值含义关键函数互斥锁保证同一时刻只有一个线程访问共享资源无二值状态pthread_mutex_init/lock/unlock/destroy信号量控制并发数量 线程同步N 可用资源数sem_init/wait/post/destroy互斥锁和信号量是多线程编程的基石。互斥锁简单高效专注于互斥信号量更灵活既能互斥又能同步。理解它们的原理和使用场景是写出正确、高效并发程序的前提。