本篇博客简单复习一下,线程之间的锁机制,主要包括自旋锁、互斥锁、条件变量、读写锁。
自旋锁
自旋锁用于保护临界区,其原理就是反复地进行 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);
int pthread_spin_trylock(pthread_spin_t *spin);
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);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
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); } 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);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);
int sem_getvalue(sem_t *sem, int *sval);
int sem_destroy(sem_t *sem);
|