线程之间的同步锁

本篇博客简单复习一下,线程之间的锁机制,主要包括自旋锁、互斥锁、条件变量、读写锁

自旋锁

自旋锁用于保护临界区,其原理就是反复地进行 Test and Set 的忙等待。这种方法会一直占用CPU资源,不让出时间片,导致其他线程无法执行。但是,在临界区代码执行时间短的情况下,自旋锁可以避免线程的上下文切换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <pthread.h>
#include <time.h>
// 初始化一个自旋锁。
int pthread_spin_init(pthread_spin_t *spin,
const pthread_spinattr_t *attr);
// 对自旋锁上锁,若自旋锁已经上锁,则调用者一直阻塞,
// 直到自旋锁解锁后再上锁。
int pthread_spin_lock(pthread_spin_t *spin);
// 调用该函数时,若自旋锁未加锁,则上锁,返回 0;
// 若自旋锁已加锁,则函数直接返回失败,即 EBUSY。
int pthread_spin_trylock(pthread_spin_t *spin);
// 当线程试图获取一个已加锁的自旋量时,pthread_spin_timedlock 自旋量
// 原语允许绑定线程阻塞时间。即非阻塞加锁自旋量。
int pthread_spin_timedlock(pthread_spin_t *restrict spin,
const struct timespec *restrict abs_timeout);
// 对指定的自旋锁解锁。
int pthread_spin_unlock(pthread_spin_t *spin);
// 销毁指定的一个自旋锁。自旋锁在使用完毕后,
// 必须要对自旋锁进行销毁,以释放资源。
int pthread_spin_destroy(pthread_spin_t *spin);

互斥锁

互斥锁也是用于保护临界区的,用法上和自旋锁是一样的,但是底层实现原理不一样。互斥锁的底层是靠操作系统的调度,当试图访问一个上锁的资源时,线程会被操作系统阻塞,直到资源被释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <pthread.h>
#include <time.h>
// 初始化一个互斥锁。
int pthread_mutex_init(pthread_mutex_t *mutex,
const pthread_mutexattr_t *attr);
// 对互斥锁上锁,若互斥锁已经上锁,则调用者一直阻塞,
// 直到互斥锁解锁后再上锁。
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 调用该函数时,若互斥锁未加锁,则上锁,返回 0;
// 若互斥锁已加锁,则函数直接返回失败,即 EBUSY。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 当线程试图获取一个已加锁的互斥量时,pthread_mutex_timedlock 互斥量
// 原语允许绑定线程阻塞时间。即非阻塞加锁互斥量。
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict abs_timeout);
// 对指定的互斥锁解锁。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 销毁指定的一个互斥锁。互斥锁在使用完毕后,
// 必须要对互斥锁进行销毁,以释放资源。
int pthread_mutex_destroy(pthread_mutex_t *mutex);

读写锁

读写锁和互斥量很相似,也是通过操作系统的调度来实现等待的。不同的是,读写锁将对资源的访问分为了读和写两种情况,从而实现对资源的更高细粒度的访问。具体的读写锁特性为:

  • 当有线程在读数据时,允许其他线程读,不允许写;
  • 当有线程在写数据时,其他线程既不能读也不能写。
    适用于对数据的读次数比写次数多的多的场景。
1
2
3
4
5
6
7
8
9
#include <pthread.h> 
int phtread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
/** 加读锁 */
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
/** 加写锁 */
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
/** 释放锁 */
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

条件变量

条件变量与其他几个锁相比较为复杂。举一个实际的场景:在生产者消费者模型中,我们希望在生产者生产出100个产品后,通知消费者执行下一步。
如果只使用互斥锁来完成这个需求,那么消费者在每次被调度时都要检查产品数量是否达到需求,那么被调度的这个过程就会占用比较多的资源,我们希望的效果是这样的:当生产者生成出100个产品时,主动通知消费者,这个时候条件变量就派上用场了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <pthread.h>
// 初始化条件变量
int pthread_cond_init(pthread_cond_t *cond,
pthread_condattr_t *cond_attr);
// 阻塞等待
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
// 超时等待
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,
const timespec *abstime);
// 解除所有线程的阻塞
int pthread_cond_destroy(pthread_cond_t *cond);
// 至少唤醒一个等待该条件的线程
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒等待该条件的所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);

一个使用的例子如下所示(略去了锁的初始化和销毁):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static int value = 0;  
static pthread_mutex_t mutex;
static pthread_cond_t condition;
void waitCondition()
{
pthread_mutex_lock(&mutex);
while (value == 0) {
pthread_cond_wait(&condition, &mutex); // 开始等待,并立即解锁 mutex
}
pthread_mutex_unlock(&mutex);
}

void triggerCondition()
{
pthread_mutex_lock(&mutex);

value = 1;

pthread_mutex_unlock(&mutex);
pthread_cond_broadcast(&condition); // 广播
}

注意 pthread_cond_wait 函数,有两个参数,所以可以知道条件变量必须配合互斥锁使用,其语义为:“暂时释放互斥锁mutex并阻塞,直到condition发生;当condition发生时,唤醒代码并重新获得mutex”。

条件变量的注意事项

  • 考虑解锁和唤醒的顺序:在触发条件时,应该注意是先触发,还是先释放锁。建议的方式是先释放锁,再触发条件,否则有可能导致触发了条件,等待方却得不到锁。
  • 要使用while而不是if:在等待方被唤醒时,有可能条件实际上是不成立的,导致虚假唤醒。

信号量

信号量本质是一个非负的整数计算器,可以用于实现同步和互斥,当其最大值为1时,可以被当做互斥锁使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <semaphore.h>
// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 信号量 P 操作(减 1)
int sem_wait(sem_t *sem);
// 以非阻塞的方式来对信号量进行减 1 操作
int sem_trywait(sem_t *sem);
// 信号量 V 操作(加 1)
int sem_post(sem_t *sem);
// 获取信号量的值
int sem_getvalue(sem_t *sem, int *sval);
// 销毁信号量
int sem_destroy(sem_t *sem);

线程之间的同步锁
http://zhouhf.top/2025/04/05/操作系统/thread-lock/
作者
周洪锋
发布于
2025年4月5日
许可协议