多线程中的同步与锁
概念
前面已经有介绍过线程的概念,所以我们知道,当两个线程同时读写一个内存区域的时候结果可能是不确定的.我们假设写操作需要两个存储器访问周期,
而读操作只需要一个访问周期.在写操作执行了一个访问周期后读操作开始执行,那么得到的结果可能并不是我们想要的.
在这个需求下,我们就需要了解锁以及同步的知识以更好的开发高性能的程序.
互斥量(mutex)
- 概念: 面对上面的需求我们可以通过互斥接口来保护数据,确保同一时间只有一个线程访问数据.mutex本质上来说是一把锁,我们在访问变量前进行
加锁,如果此时有任何线程试图对相同的mutex执行加锁操作都会被阻塞,直到完成操作后我们对mutex解锁.此时因为锁被阻塞的线程会被唤醒. -
数据类型:互斥变量是用pthread_mutex_t数据类型表示的,下面是初始化以及销毁互斥变量的函数原型
#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
//参数attr设置为NULL则为默认属性
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//两个函数若成功则返回0,否则返回错误编号 -
互斥量操作函数原型:
#include<pthread.h>
int pthread_mutex_lock(pthread_t *mutex)
int pthread_mutex_trylock(pthread_t *mutex)
int pthread_mutex_unlock(pthread_t *mutex)
//所有函数若成功则返回0,否则返回错误编号- lock函数:对mutex变量进行加锁操作,如果此时已有进程对此mutex加锁则阻塞直到mutex被解锁
- trylock函数:如果不希望线程被阻塞,可以调用trylock函数尝试对mutex进行加锁操作.如果此时mutex已加锁,则返回EBUSY表示失败.
- unlock函数:对mutex变量进行解锁操作.
超时互斥量
- 假如我们希望访问一个受到保护的变量,但是又希望对这个访问做出一个限制:如果等待5s仍旧无法访问我们就放弃这次访问.此时我们就需要用到
这个超时互斥量接口. -
函数原型:
#include<time.h>
#include<pthread.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict tsptr);
//成功返回0,失败则返回错误编号. 在超时到来前mutex被解锁,则函数行为与lock一致.如果超时则返回ETIMEDOUT.
读写锁
假如我们需要保护的变量被修改的次数远远小于被读取的次数,此时我们再使用mutex就会对性能造成一些浪费.因为在大量的读操作中并不会造成
乱序问题.在这种情况下我们就可以利用读写锁来减少加锁造成的性能损失.-
读写锁通过结构体pthread_rwlock_t表示,不同于mutex,读写锁在使用之前必须进行初始化,在释放底层内存前必须销毁.
#include<pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//两个函数成功则返回0,否则返回错误编号. -
函数原型:
#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);
//所有函数成功则返回0,否则返回错误编号. - pthread_rwlock_rdlock:读锁,允许n(根据实现决定)个读锁同时锁定.
- pthread_rwlock_wrlock:写锁,当读锁存在时加写锁会造成进程阻塞.同时为了避免出现饥饿的情况,写锁的状态会阻塞住后面的读锁.
也就是说,即使写锁是被阻塞的,同样也会阻止其他线程再添加读锁. pthread_rwlock_unlock:不论以哪种形式加锁都可以通过unlock解锁.如果但从函数表现来看,应该是锁内部维护了状态以及计数器,如果是
读锁则将锁数量减1,如果是写锁则切换锁状态(Go语言的实现方法,我并没有查过C语言的实现源码)-
读写锁原语:
#include<pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//两个函数成功则返回0,否则返回错误编号.行为等同于mutex的锁原语.
条件变量
当我们的需求不仅仅是锁住某个临界区,并且还需要判断某些条件是否成立,这个时候条件变量是比mutex更好的选择.
为什么要用条件变量:
我们来假设这样一个场景,四个线程读取缓冲区,两个线程写入缓冲区.假如此时缓冲区为空,并且写入线程阻塞等待数据,这种情况下四个读取线程会做什么呢?它们会不停的循环进行加锁-判断缓冲区内容-缓冲区为空-解锁.
这样的频繁加锁解锁的操作很大程度上浪费了CPU资源,所以此时我们需要引入条件变量来帮助我们解决这一问题.-
初始化:
#include<pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *restrict cond);
//成功则返回0,失败则返回错误代码- 使用条件变量前必须进行初始化.
-
wait:
#include<pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex,
const struct timespec *restrict tsptr);
//成功则返回0,失败则返回错误代码wait利用mutex以及条件变量对线程进行阻塞,调用者将锁住的互斥量传递给函数,函数将线程放在等待条件的线程列表上并对互斥量解锁.这样线程就不会错过任何条件变化.当函数返回时,该条件再次
被加锁.timedwait对wait增加了超时限制(tsptr参数).
-
唤醒:
#include<pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
//成功则返回0,失败则返回错误代码- 通过这两个函数唤醒等待条件的进程,signal至少能唤醒一个,而broadcast则能唤醒全部等待条件的进程.
自旋锁
自旋锁与mutex类似,都是通过阻塞的形式阻止获得已被加锁的锁.
mutex被阻塞时直接陷入睡眠等待信号(sleep-waiting),而自旋锁则是忙等待(busy-waiting),也就是说自旋锁的等待是占用CPU的
-
自旋锁初始化:
#include<pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
//两个函数若成功则返回0,否则返回错误编号- pshared参数:设置为PTHREAD_PROCESS_SHARED则可以被不同的进程共享,PTHREAD_PROCESS_PRIVATE只能被初始化锁的内部线程所访问
-
函数原型:
#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);
//所有函数如果成功则返回0,失败返回错误编号 需要注意的是,使用自旋锁的范围其实很窄,除非频繁切换线程的开销非常大或者我们持有锁的时间非常短,否则使用自旋锁对cpu的占用其实很高.
屏障
屏障是用户协调多个线程并行工作的同步机制.屏障允许每个线程等待,直到所有的线程都到达某一点(和go里面的WaitGroup一样).
-
初始化:
#include<pthread.h>
int pthread_barrier_init(pthread_barrier_t *restrict barrier, const pthread_barrierattr_t *restrict attr,unsigned int count);
int pthread_barrier_destroy(pthread_barrier_t *barrier);
//两个函数若成功则返回0,否则返回错误编号 -
函数原型:
#include<pthread.h>
int pthread_barrier_wait(pthread_barrier_t *barrier);
//成功则返回0或者PTHREAD_BARRIER_SEGIAL_THREAD,否则返回错误编号 调用wait的函数会在条件不满足时阻塞,直到count计数满足之后所有线程被唤醒.
需要注意的是,到达屏障计数后屏障可以被重置,但此时的屏障计数没有改变.所以我们想要重用需要destroy然后再init初始化.