本文主要参考文献是正点原子的C应用编程指南,很多内容与正点原子文档中的一致。作为初学者,为了和大家一起学习所以准备写这一系列文章,也是作为自己学习的一个笔记,如果有错误欢迎大家提出来一起讨论。
线程同步
在多线程程序中,多个线程会共享进程资源,其中最典型的就是全局变量或静态变量 。当多个线程并发 地对同一共享资源进行读写操作时,如果缺乏必要的协调机制,就会导致数据不一致 的问题。这种由并发访问引发的数据错乱,本质上是竞争条件(Race Condition) 的体现。
为了直观理解这个问题,我们假设两个线程同时对同一个全局变量 g_count 进行自增操作。在 C 语言中,g_count++ 看似是一条指令,但在底层实际由以下三个步骤构成:
- 从内存中读取
g_count的值到 CPU 寄存器(读)。 - 在寄存器中将值加 1(改)。
- 将寄存器中的新值写回内存(写)。
如果线程 A 在执行完步骤 2 后,还没来得及将新值写回内存(步骤 3),此时线程 B 被调度执行并完整地完成了"读-改-写"操作。当线程 A 最终恢复执行并将自己的结果写回时,它会覆盖掉线程 B 已经写入的值 。这就导致即使两个线程各自自增了一次,g_count 的最终值却只增加了 1,而不是预期的 2。
解决这一问题的核心思路是:将"读-改-写"这三个步骤合并为一个不可分割的原子操作。 也就是说,当一个线程开始操作该共享变量时,其他线程必须等待,直到当前线程完成整个操作。这种机制被称为线程同步。Linux 提供了多种实现线程同步的机制,包括本章将要介绍的互斥锁、条件变量、自旋锁以及读写锁等。
数据不一致的根本原因在于,进程中的多个线程是并发执行的,系统调度器会在任意时刻挂起或恢复线程。如果没有同步机制,就无法预测多个线程对共享资源的访问顺序,从而导致计算结果不确定。线程同步的目的就是通过强制规定访问顺序来消除这种不确定性。
互斥锁
互斥锁(Mutex,Mutual Exclusion)是 Linux 线程同步中最基础、最核心的机制。从本质上说,它是一把"建议锁",用于保护共享资源的访问。在访问共享资源之前,线程必须先对互斥锁进行上锁;访问完成后释放锁。如果在尝试上锁时锁已被其他线程持有,则调用线程会进入阻塞状态,直到锁被释放。
互斥锁的数据类型和相关操作宏定义在 <pthread.h> 头文件中。其核心数据类型和初始化宏定义如下:
c
#include <pthread.h>
/* 互斥锁数据类型:不透明结构体,不应直接访问其内部成员 */
pthread_mutex_t mutex;
/* 静态初始化宏:使用默认属性在定义时直接初始化 */
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
/* 互斥锁属性类型(用于 pthread_mutex_init 的第二个参数) */
pthread_mutexattr_t attr;
/* 互斥锁类型属性(用于 pthread_mutexattr_settype)*/
PTHREAD_MUTEX_NORMAL /* 标准锁:不支持自旋检测,同一线程重复加锁会导致死锁 */
PTHREAD_MUTEX_ERRORCHECK /* 错误检查锁:同一线程重复加锁或解锁其他线程的锁会返回错误 */
PTHREAD_MUTEX_RECURSIVE /* 递归锁:允许同一线程多次加锁,需相同次数解锁 */
PTHREAD_MUTEX_DEFAULT /* 默认锁:在 Linux 上行为等同于 NORMAL */
互斥锁初始化
互斥锁的数据类型为 pthread_mutex_t,在使用之前必须进行初始化。根据互斥锁的分配方式(静态或动态),初始化方法分为两种。对于静态分配 的互斥锁(如全局变量或 static 变量),可以直接使用宏 PTHREAD_MUTEX_INITIALIZER 在定义时完成初始化。对于动态分配 的互斥锁(如通过 malloc() 分配在堆上的对象,或需要自定义属性的场景),则必须调用 pthread_mutex_init() 函数进行初始化。当互斥锁不再需要时,应调用 pthread_mutex_destroy() 销毁它,以释放其占用的资源。不能销毁一个尚未解锁的互斥锁,也不能销毁一个未初始化的互斥锁。
c
#include <pthread.h>
/* 互斥锁类型 */
pthread_mutex_t mutex;
/* 静态初始化宏(仅适用于定义时直接初始化) */
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
/* 动态初始化与销毁 */
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
mutex(init/destroy):指向目标互斥锁对象。attr(仅init):指向pthread_mutexattr_t对象,用于设置互斥锁的属性(如类型、进程共享等)。传入NULL表示使用默认属性。- 返回值 :成功返回
0;失败返回错误码。
初始化方式的匹配原则 :使用
PTHREAD_MUTEX_INITIALIZER初始化的互斥锁无需(也不能)再调用pthread_mutex_init()。对于动态分配(如malloc返回)或需要在运行时设置属性的互斥锁,必须使用pthread_mutex_init()。两者不可混用,否则会导致未定义行为。
互斥锁加锁和解锁
pthread_mutex_lock() 用于对互斥锁进行上锁。如果互斥锁当前处于未锁定状态,调用线程将立即获得锁并返回;如果锁已被其他线程持有,调用线程将阻塞等待 ,直到锁被释放。pthread_mutex_unlock() 用于释放当前线程持有的互斥锁。解锁操作必须由持有该锁的线程执行,且不能对已处于未锁定状态的互斥锁进行解锁(除非锁类型为 ERRORCHECK,会返回错误),否则会导致未定义行为。
c
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
mutex:指向目标互斥锁。- 返回值 :成功返回
0;失败返回错误码。
尝试解锁: pthread_mutex_trylock() 函数
如果线程不希望因锁被占用而陷入阻塞,可以使用 pthread_mutex_trylock()。该函数尝试对互斥锁进行加锁。如果锁处于未锁定状态,调用将成功获取锁并返回 0;如果锁已被其他线程持有,函数不会阻塞 ,而是立即返回错误码 EBUSY,调用者可以根据该返回值决定后续逻辑(如稍后重试或放弃任务)。
c
#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
mutex:指向目标互斥锁。- 返回值 :成功返回
0;如果互斥锁已被其他线程锁定,返回EBUSY;其他错误返回错误码。
核心使用示例
以下代码展示了使用互斥锁保护共享变量 g_count 的核心逻辑。上方为使用 pthread_mutex_lock() 的标准阻塞模式,下方为使用 pthread_mutex_trylock() 的非阻塞轮询模式:
c
/* 标准阻塞模式 */
pthread_mutex_lock(&mutex);
/* 临界区:安全操作共享资源 */
g_count++;
pthread_mutex_unlock(&mutex);
/* 非阻塞轮询模式 */
while (pthread_mutex_trylock(&mutex)) ; /* 加锁失败则立即重试,消耗 CPU */
/* 可以在while中执行一些指令,隔一段时间再查询是否解锁*/
/* 临界区 */
g_count++;
pthread_mutex_unlock(&mutex);
注意事项
互斥锁是"建议锁",而非"强制锁" :互斥锁的有效性依赖于所有线程的自觉遵守。如果某个线程在没有加锁的情况下直接访问共享资源,互斥锁无法阻止它。因此,所有访问该共享资源的线程都必须遵循相同的加锁规则,否则数据不一致问题依然会发生。这类似于交通信号灯------它无法强行拦下闯红灯的车辆,但如果所有人都遵守规则,系统就能正常运转。
使用while(pthread_mutex_trylock(&mutex));实现了"自旋"效果,但这种方式会持续占用 CPU 资源(导致 CPU 使用率飙升至 100%)。在实际开发中,除非临界区极短且锁竞争极少,否则应优先考虑阻塞模式的pthread_mutex_lock(),或者结合条件变量(后续学)实现更高效的无阻塞等待。
解锁操作的归属检查 :虽然pthread_mutex_unlock()不会显式检查调用者是否是该锁的持有者(NORMAL类型下),但解锁其他线程持有的锁属于未定义行为,可能导致锁状态混乱或程序崩溃。ERRORCHECK类型的互斥锁会捕获此类错误并返回EPERM,是调试阶段的有力工具。
销毁互斥锁
当互斥锁不再被使用时,应当调用 pthread_mutex_destroy() 将其销毁,以释放其占用的系统资源。不能销毁一个尚未解锁的互斥锁(即仍被某个线程持有),也不能销毁一个尚未初始化的互斥锁。 被销毁后的互斥锁不能再被用于加锁或解锁操作;如果再次使用,必须重新调用 pthread_mutex_init() 对其进行初始化。
c
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
mutex:指向需要销毁的目标互斥锁。- 返回值 :成功返回
0;失败返回错误码。
互斥锁死锁
死锁 是多线程编程中最严重的错误之一,指两个或多个线程因相互等待对方持有的资源而永久阻塞,无法继续执行。互斥锁的死锁主要出现在以下两种场景中:
场景一:同一线程对同一个 NORMAL 类型互斥锁重复加锁 (对ERRORCHECK类型的重复加锁不会死锁,但是会报错)。如果线程在持有锁的情况下再次调用 pthread_mutex_lock() 锁定同一个锁,该线程会立即陷入永久阻塞(因为它正等待自己释放一把永远不会释放的锁),从而导致死锁。
场景二:多个线程以不同的顺序请求多个锁。例如,线程 A 持有锁 1 并请求锁 2,而线程 B 持有锁 2 并请求锁 1。此时两个线程都在等待对方释放自己需要的锁,形成循环等待,导致双方永久阻塞。
避免死锁的常用方法包括:
- 固定加锁顺序(锁层级) :当多个线程需要访问同一组互斥锁时,强制所有线程以完全相同的顺序 获取这些锁。例如,所有线程必须先锁
mutex1再锁mutex2,这样可以有效消除循环等待。 - 非阻塞尝试 + 回退重试 :使用
pthread_mutex_trylock()尝试获取第二个锁。如果尝试失败(返回EBUSY),线程立即释放已经持有的所有锁,等待一段时间后从头再试。这种方法效率较低但逻辑更灵活,适用于复杂的锁依赖场景。
互斥锁的属性
互斥锁的行为可以通过 pthread_mutexattr_t 属性对象进行精细控制。在使用属性对象之前,必须调用 pthread_mutexattr_init() 进行初始化;当不再需要时,调用 pthread_mutexattr_destroy() 销毁。在所有互斥锁属性中,类型属性(Type Attribute) 最为常用,它直接决定了互斥锁的锁定特性。调用 pthread_mutexattr_settype() 可以设置类型属性,而 pthread_mutexattr_gettype() 用于获取当前类型。
c
#include <pthread.h>
/* 属性对象的初始化与销毁 */
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
/* 类型属性的设置与获取 */
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
attr:指向互斥锁属性对象。type(settype):指定锁类型,可取以下宏之一:PTHREAD_MUTEX_NORMAL:标准锁。同一线程重复加锁会导致永久死锁;解锁其他线程持有的锁或已解锁的锁将导致未定义行为。PTHREAD_MUTEX_ERRORCHECK:错误检查锁。所有非法操作(如同线程重复加锁、解锁其他线程的锁、解锁已解锁的锁)都会立即返回错误码,不会死锁。适合调试阶段使用,但性能略低。PTHREAD_MUTEX_RECURSIVE:递归锁。允许同一线程多次加锁 (锁内部维护计数。也就是第二次加锁时不会等待阻塞第一个锁的解锁,从而避免死锁),但必须确保解锁次数与加锁次数相等才能释放锁。适合在递归函数或嵌套调用中保护共享资源。PTHREAD_MUTEX_DEFAULT:默认锁。在 Linux 系统上其行为与NORMAL类型相同,但为了可移植性,不应依赖其具体行为。
type(gettype):返回当前锁类型。- 返回值 :成功返回
0;失败返回错误码。
递归锁的典型应用场景 :在递归函数中,同一线程可能在不同的递归层级中多次进入临界区。如果使用
NORMAL锁,第二次进入时会导致死锁;使用RECURSIVE锁则可以安全地允许这种情况,因为锁会记录持有者的线程 ID 和加锁次数,只有解锁次数等于加锁次数时才真正释放锁。但递归锁的性能开销略高于普通锁,且在逻辑设计上有时会掩盖不合理的代码结构,应谨慎使用。
使用示例
以下代码展示了如何创建并初始化一个 ERRORCHECK 类型的互斥锁,以便在调试阶段捕获非法加锁/解锁操作:
c
pthread_mutex_t mutex;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);
pthread_mutex_init(&mutex, &attr);
/* 使用互斥锁... */
pthread_mutex_destroy(&mutex);
pthread_mutexattr_destroy(&attr);
注意事项
递归锁的计数机制 :
RECURSIVE锁允许同一线程成功加锁多次。每成功加锁一次,内部计数器加 1;每解锁一次,计数器减 1。只有当计数器归零时,锁才真正被释放,其他线程才能获取该锁。如果解锁次数少于加锁次数,锁将永远无法释放,导致死锁。
ERRORCHECK类型在每次加锁/解锁时都会进行额外的合法性检查,这会引入轻微的性能开销,但能极大提高调试效率。在性能敏感的最终产品中,通常会使用NORMAL或DEFAULT类型以最大化吞吐量。RECURSIVE类型虽然功能强大,但因其内部维护计数和归属检测,开销也高于NORMAL类型。
条件变量
条件变量(Condition Variable)是另一种线程同步机制,用于自动阻塞线程,直到某个特定条件成立或某个事件发生 。它通常与互斥锁配合使用------互斥锁用于保护共享数据,条件变量则用于在条件不满足时让线程进入休眠状态,避免无效的忙等待(Busy Waiting)。条件变量主要包含两个动作:等待 (阻塞直到条件满足)和通知(唤醒正在等待的线程)。如果没有任何线程在等待,发送的通知信号会被直接丢弃。
使用条件变量通常是因为:持有互斥锁的同时还需要满足某些条件才可以继续执行。此时持有锁,但是又需要循环等待条件满足,为了提高系统执行的效率,可以把循环等待条件这个过程变为阻塞态,防止条件始终不满足从而死锁。
本小节提到的"信号"并非指之前讲到的信号的概念
条件变量初始化
条件变量的数据类型为 pthread_cond_t,使用前必须进行初始化。静态分配 的条件变量可以使用宏 PTHREAD_COND_INITIALIZER 在定义时初始化。动态分配 或需要自定义属性的条件变量则必须调用 pthread_cond_init() 进行初始化。当不再需要时,应调用 pthread_cond_destroy() 销毁它。仅当没有任何线程在等待该条件变量时,销毁才是安全的;对已初始化的条件变量再次初始化会导致未定义行为。
c
#include <pthread.h>
/* 条件变量类型 */
pthread_cond_t cond;
/* 静态初始化宏 */
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
/* 动态初始化与销毁 */
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);
cond:指向条件变量对象。attr(仅init):指向pthread_condattr_t对象,用于设置条件变量的属性(如进程共享、时钟类型)。传入NULL表示使用默认属性。- 返回值 :成功返回
0;失败返回错误码。
通知和等待条件变量
pthread_cond_wait() 用于阻塞调用线程,直到收到条件变量的通知。该函数必须在互斥锁被锁定的情况下调用,它会自动执行两个原子操作:
- 解锁互斥锁,让其他线程能够访问共享数据;
- 将调用线程挂起,等待条件变量被通知。当线程被唤醒时,
pthread_cond_wait()会在返回之前自动重新锁定互斥锁,确保线程在返回后持有锁。
pthread_cond_signal() 用于唤醒至少一个 正在等待该条件变量的线程(如果有多个,具体唤醒哪个由调度策略决定)。pthread_cond_broadcast() 则用于唤醒所有 正在等待的线程。无论使用哪个函数,被唤醒的线程在从 pthread_cond_wait() 返回前都必须重新获取互斥锁,因此实际上同一时刻只有一个线程能继续执行,其余线程会继续阻塞在锁上。
c
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
cond:指向条件变量对象。mutex(仅wait):指向一个已锁定的互斥锁。函数内部会原子地解锁它并进入等待。- 返回值 :成功返回
0;失败返回错误码。
pthread_cond_wait()的原子性 :它的"解锁 + 挂起"操作是原子 的,这意味着在互斥锁被释放和线程进入休眠状态之间,不会发生其他线程获取该锁并发出信号的情况。这避免了之前提到的sigprocmask+pause竞争条件的类似问题( 当时是使用sigsupend()原子操作解决的 )。
如果调用pthread_cond_signal()或pthread_cond_broadcast()时,没有线程正在等待该条件变量,该信号会被直接丢弃 ,不会有任何效果。后续线程调用pthread_cond_wait()会正常阻塞,不会因为之前的信号而被唤醒。这与实时信号的排队机制不同------条件变量不维护信号历史。
signal与broadcast的选择 :pthread_cond_signal()至少唤醒一个线程,效率更高,适合只有一个线程需要处理新数据的场景。pthread_cond_broadcast()唤醒所有线程,适合多个线程需要同时响应状态变化(如系统配置更新)的场景。即使使用broadcast,由于互斥锁的存在,所有被唤醒的线程仍会逐个串行执行。
条件变量的判断条件:必须使用 while 循环
pthread_cond_wait() 必须始终放在 while 循环中,而不能使用 if 语句。原因在于:
- 虚假唤醒(Spurious Wakeup) :POSIX 标准允许
pthread_cond_wait()在没有收到明确信号的情况下被唤醒(即虚假唤醒)。虽然实际系统中很少发生,但为了标准可移植性,必须加以应对。 - 多个消费者竞争 :当多个线程同时等待同一个条件变量时,
signal唤醒其中一个线程。该线程获取锁并修改共享资源后(如将g_avail减为 0),其他被broadcast唤醒的线程在获取锁后检查条件时会发现条件已不再成立(如g_avail == 0),必须重新进入等待状态。
采用
while的方法可以使程序在从wait函数出来之后再次判断是否g_avail真的满足条件了可以向下执行。而if会导致在上述条件下无法在此检查条件是否满足,从而继续执行了接下来的代码。
c
/* 错误写法(if 语句):存在风险 */
if (0 >= g_avail)
pthread_cond_wait(&cond, &mutex);
/* 正确写法(while 循环):标准做法 */
while (0 >= g_avail)
pthread_cond_wait(&cond, &mutex);
虚假唤醒是 POSIX 标准允许的,目的是简化底层实现。即使在 Linux 上很少发生,依赖
if语句的代码在其他系统上可能随机崩溃。因此,使用while循环是所有遵循 POSIX 标准程序必须遵守的编码规范。
条件变量的属性
条件变量的属性通过 pthread_condattr_t 对象设置,主要包括进程共享属性 和时钟属性 。进程共享属性控制条件变量是否能在多个进程间共享(默认仅限同一进程内使用),时钟属性控制 pthread_cond_timedwait() 使用的超时时钟源。在绝大多数应用中,传入 NULL 使用默认属性已足够。
使用示例
以下代码展示了条件变量配合互斥锁的核心逻辑:
c
static pthread_mutex_t mutex;
static pthread_cond_t cond;
static int g_avail = 0;
/* 消费者线程 */
static void *consumer_thread(void *arg)
{
for ( ; ; ) {
pthread_mutex_lock(&mutex);
while (0 >= g_avail) /* ★ while 循环判断条件,防止虚假唤醒 */
pthread_cond_wait(&cond, &mutex); /* 原子解锁 + 挂起等待 */
g_avail--; /* 消费 */
pthread_mutex_unlock(&mutex);
}
}
/* 生产者(主线程) */
for ( ; ; ) {
pthread_mutex_lock(&mutex);
g_avail++; /* 生产 */
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond); /* 通知消费者(唤醒至少一个等待线程) */
}
cond变量不需要赋任何值,只是起到一个变量中介(用于通知)的作用。cond变量表示由用户定义的任意条件,类似于在编程时使用的flag变量。
注意事项
条件变量必须与互斥锁配套使用 :条件的检测和共享资源的修改都必须在互斥锁的保护下进行。
pthread_cond_wait()会在挂起前自动释放互斥锁,唤醒后自动重新获取,这一原子操作保证了共享数据的一致性,防止了"检查条件"和"进入休眠"之间的竞争窗口。
互斥锁的持有与条件变量信号之间的关系 :pthread_cond_signal()可以在持有锁的情况下 调用,也可以在释放锁之后调用。
- 在持有锁时调用,被唤醒的线程在获取锁时会立即返回;
- 在解锁后调用,被唤醒的线程需要等待生产者释放锁后才能进入临界区。
这两种方式在逻辑上都是正确的,后者通常能减少锁持有时间,提升并发性能。
对于前者,流程是这样的:
- 消费者(等待者)被信号唤醒。
- 消费者试图从
pthread_cond_wait()返回,但它必须重新获取互斥锁才能返回。- 然而,此时锁还被生产者持有。所以消费者只能阻塞在锁上(等待锁被释放)。
- 生产者执行
pthread_mutex_unlock(&mutex)。- 锁一释放,消费者立刻抢到锁,
pthread_cond_wait()立即返回。这里的"立即"是指: 一旦锁被释放,消费者线程几乎没有任何延迟就能获得锁并返回,因为内核已经标记它为"优先唤醒"状态。
条件变量不具备状态记忆能力 :如果在pthread_cond_wait()之前调用了pthread_cond_signal(),该信号会丢失。这意味着程序不能依赖条件变量来保存状态,状态的保存必须通过共享数据(如g_avail)来实现。条件变量只是一个"通知机制",而非"状态存储机制"。
自旋锁
自旋锁(Spin Lock)是另一种用于保护共享资源的同步机制,从功能上看与互斥锁非常相似------在访问共享资源前上锁,访问完成后解锁。但它们的实现原理截然不同:
- 如果互斥锁已被其他线程持有,尝试加锁的线程会进入休眠(阻塞) ,交出 CPU 使用权;
- 而如果自旋锁已被其他线程持有,尝试加锁的线程会在 CPU 上原地"自旋"(忙等待) ,持续循环检查锁是否被释放,直到成功获取锁为止。
自旋锁不会让出 CPU,因此适用于临界区极短 的场景------持有锁的时间非常短(几条指令),这样自旋等待的线程只需等待极短时间就能获得锁,避免了线程休眠和唤醒带来的上下文切换开销,效率远高于互斥锁。但如果临界区较长,自旋锁会导致 CPU 长时间空转,造成严重的资源浪费。自旋锁的数据类型和相关操作宏定义在 <pthread.h> 头文件中。
c
#include <pthread.h>
/* 自旋锁数据类型:不透明结构体 */
pthread_spinlock_t spin;
/* 进程共享属性(用于 pthread_spin_init 的 pshared 参数) */
PTHREAD_PROCESS_SHARED /* 自旋锁可在多个进程间的线程共享 */
PTHREAD_PROCESS_PRIVATE /* 自旋锁仅限本进程内线程使用(默认) */
自旋锁初始化与销毁
自旋锁使用前必须调用 pthread_spin_init() 进行初始化。参数 pshared 控制自旋锁的进程共享属性:PTHREAD_PROCESS_SHARED 表示锁可跨进程共享(需配合共享内存),PTHREAD_PROCESS_PRIVATE(默认)表示锁仅在本进程内使用。当不再需要自旋锁时,应调用 pthread_spin_destroy() 销毁它。
c
#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
lock:指向自旋锁对象。pshared(仅init):指定进程共享属性,取PTHREAD_PROCESS_SHARED或PTHREAD_PROCESS_PRIVATE。- 返回值 :成功返回
0;失败返回错误码。
自旋锁加锁和解锁
pthread_spin_lock() 用于对自旋锁进行加锁。如果锁处于未锁定状态,调用线程将立即获得锁并返回;如果锁已被其他线程持有,调用线程将在原地循环"自旋" ,直到锁被释放。pthread_spin_trylock() 尝试加锁,如果锁已被其他线程持有,不会自旋等待 ,而是立即返回错误码 EBUSY。pthread_spin_unlock() 用于释放当前线程持有的自旋锁。
试图对同一自旋锁重复加锁(即使在同一线程中)会导致永久死锁,因为自旋锁不记录持有者信息。
c
#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
lock:指向自旋锁对象。- 返回值 :成功返回
0;pthread_spin_trylock()在锁已被持有时返回EBUSY;其他错误返回错误码。
由于自旋锁的"忙等待"特性,在用户态应用程序中应谨慎使用。如果临界区执行时间超过一次上下文切换的时间,使用互斥锁通常更高效。用户态自旋锁更适合以下情况:临界区极短(仅几条汇编指令)、锁竞争较少、系统是多核处理器(自旋线程可以在其他核心上运行)。在单核处理器上,自旋锁在等待期间会占用唯一的核心,除非锁持有者被抢占,否则将造成死锁,因此单核系统上自旋锁几乎无效。
自旋锁的典型应用场景(内核态) :自旋锁在内核代码中应用广泛,尤其在中断服务函数(ISR) 中------因为中断服务函数不能休眠(睡眠会导致无法返回中断上下文),互斥锁可能会引发休眠,而自旋锁不会,因此是内核中断上下文的唯一选择。这是文档明确指出的自旋锁与互斥锁在使用场景上的重要差异。
使用示例
以下代码展示了使用自旋锁保护共享变量 g_count 的核心逻辑,效果与互斥锁完全相同,但性能表现取决于临界区的长短:
c
static pthread_spinlock_t spin;
static int g_count = 0;
static void *thread_func(void *arg)
{
for (int j = 0; j < loops; j++) {
pthread_spin_lock(&spin); /* 临界区开始,短临界区时效率高 */
g_count++; /* 假设这里只有几条简单指令 */
pthread_spin_unlock(&spin); /* 临界区结束 */
}
return NULL;
}
读写锁
读写锁(Reader-Writer Lock)是互斥锁的升级版,针对 "读多写少" 的场景进行了专门优化。与互斥锁只有"锁定"和"未锁定"两种状态不同,读写锁有三种状态:读模式加锁 、写模式加锁 和未加锁 。其核心规则是:写锁是独占的 (一次只能有一个线程持有写锁,且写加锁期间阻塞所有其他读写请求),而读锁是共享的 (允许多个线程同时持有读锁)。这种设计允许多个线程并发读取共享数据而互不阻塞,从而比互斥锁具有更高的并行性。读写锁的数据类型和相关操作宏定义在 <pthread.h> 头文件中。
c
#include <pthread.h>
/* 读写锁数据类型:不透明结构体 */
pthread_rwlock_t rwlock;
/* 静态初始化宏:使用默认属性在定义时直接初始化 */
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
/* 读写锁属性类型(用于 pthread_rwlock_init 的第二个参数) */
pthread_rwlockattr_t attr;
/* 进程共享属性(用于 pthread_rwlockattr_setpshared) */
PTHREAD_PROCESS_SHARED /* 读写锁可在多个进程间的线程共享 */
PTHREAD_PROCESS_PRIVATE /* 读写锁仅限本进程内线程使用(默认) */
读写锁初始化与销毁
读写锁使用前必须进行初始化。可以使用静态初始化宏 PTHREAD_RWLOCK_INITIALIZER(仅适用于定义时立即初始化),或者调用 pthread_rwlock_init() 进行动态初始化(适用于堆分配、全局变量或需要自定义属性的场景)。当不再需要读写锁时,应调用 pthread_rwlock_destroy() 销毁它。与互斥锁相同:不能销毁一个尚未解锁的读写锁,也不能销毁一个未初始化的读写锁。
c
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
rwlock:指向需要初始化或销毁的读写锁对象。attr(仅init):指向pthread_rwlockattr_t对象,用于设置读写锁的属性(如进程共享)。传入NULL表示使用默认属性。- 返回值 :成功返回
0;失败返回错误码。
读写锁上锁和解锁
pthread_rwlock_rdlock() 以读模式加锁,允许多个线程同时持有(共享)。如果读写锁已被写模式锁定,调用线程会阻塞等待写锁释放。
pthread_rwlock_wrlock() 以写模式加锁,一次只能有一个线程持有(独占)。如果读写锁已被其他线程以读或写模式锁定,调用线程会阻塞等待所有锁释放。
pthread_rwlock_unlock() 用于解锁(无论是读模式还是写模式,都使用同一个解锁函数)。当读写锁处于读模式时,如果有线程尝试获取写锁,该写锁请求会被阻塞,后续的读锁请求也会被阻塞(优先让写锁先执行)。
c
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
rwlock:指向读写锁对象。- 返回值 :成功返回
0;失败返回错误码。
如果线程不希望因锁被占用而阻塞,可以使用非阻塞版本。pthread_rwlock_tryrdlock() 尝试以读模式加锁,如果锁已被写模式锁定,立即返回 EBUSY。pthread_rwlock_trywrlock() 尝试以写模式加锁,如果锁已被其他线程以读或写模式锁定,立即返回 EBUSY。
c
#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
rwlock:指向读写锁对象。- 返回值 :成功返回
0;如果锁已被其他线程以不兼容模式持有,返回EBUSY;其他错误返回错误码。
读写锁属性
读写锁的唯一属性是进程共享属性 ,通过 pthread_rwlockattr_t 对象进行管理。调用 pthread_rwlockattr_init() 初始化属性对象,pthread_rwlockattr_destroy() 销毁它。pthread_rwlockattr_getpshared() 获取当前共享属性,pthread_rwlockattr_setpshared() 设置共享属性。
c
#include <pthread.h>
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);
attr:指向读写锁属性对象。pshared(获取):返回当前进程共享属性值。pshared(设置):指定进程共享属性,可取PTHREAD_PROCESS_SHARED(跨进程)或PTHREAD_PROCESS_PRIVATE(仅本进程,默认)。- 返回值 :成功返回
0;失败返回错误码。
使用示例
以下代码展示了读写锁的核心使用模式。5 个读线程并发读取 g_count 而不互相阻塞,5 个写线程依次独占写入:
c
static pthread_rwlock_t rwlock;
static int g_count = 0;
/* 读线程:多个线程可以同时执行 */
static void *read_thread(void *arg)
{
pthread_rwlock_rdlock(&rwlock); /* 以读模式加锁(共享) */
printf("g_count = %d\n", g_count); /* 多个读线程可同时进入 */
pthread_rwlock_unlock(&rwlock);
return NULL;
}
/* 写线程:一次只能有一个线程执行 */
static void *write_thread(void *arg)
{
pthread_rwlock_wrlock(&rwlock); /* 以写模式加锁(独占) */
g_count += 20; /* 写入时阻塞其他所有读写操作 */
pthread_rwlock_unlock(&rwlock);
return NULL;
}
注意事项
读写锁的适用场景 :读写锁非常适合于共享数据读操作远多于写操作的场景。例如数据库缓存、配置表等。如果读写比例接近 1:1,或写操作频繁,读写锁的复杂性和开销可能超过互斥锁,反而导致性能下降。
性能权衡:读写锁虽然提供了更高的并行性,但其内部维护了读计数器和写等待队列,因此单次加锁/解锁的开销比互斥锁略大。因此,只有在读操作比例确实很高(如 80% 以上)时,读写锁才比互斥锁更有优势。在其他情况下,互斥锁可能更高效。
写锁请求会阻塞后续读锁(防止写饥饿) :当读写锁处于读模式时,如果有线程正在等待获取写锁,后续尝试获取读锁的线程会被阻塞 (即使当前还有读锁持有者未释放)。这是为了防止写线程"饿死" ------如果读操作源源不断,写线程可能永远无法获得锁。这种设计偏好写操作,确保数据更新能被及时执行。
同一线程的递归加锁问题 :读写锁不支持递归加锁 。如果同一线程先以读模式加锁,再尝试以写模式加锁(或反之),会导致死锁 。与PTHREAD_MUTEX_RECURSIVE不同,读写锁没有提供递归选项,必须在代码逻辑层确保不会发生嵌套加锁。