线程为什么要同步?
当进程中的多个线程,同时读取一块内存数据,与此同时其中一个或多个线程修改了这块内存数据。这样就会导致不可预期的结果。
因为线程不安全引起的错误往往非常难发现,不能稳定复现,所以在编码的时候就应该事先考虑,会不会有多线程并发的情况,就像我们写完malloc之后就会去判空,然后再memset一样,多线程操作环境,就应该选择互斥锁或其他方式,保护共享资源
问题示意图:
Linux下常用的线程间同步方式有五种,分别是互斥锁、读写锁、条件变量、自旋锁、屏障
1.互斥锁(mutex)
什么都可以不看,最后的死锁总结一定要看!
1.1 pthread_mutex_init初始化互斥锁
// 动态分配
pthread_mutex_t lock;
pthread_mutex_init(&lock, NULL);
// 静态分配
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
锁必须采用其中一种来初始化,两种方式使用下来没啥差别
1.2 pthread_mutex_destroy销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//成功,返回0;否则,返回错误编号
1.3 pthread_mutex_lock上锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//所有函数的返回值:若成功,返回0;否则,返回错误编号
lock:将一直阻塞,直到拿到锁。如果重复lock而没有unlock则会造成死锁,程序阻塞,这是程序开发中常见的问题。
trylock:尝试上锁,不阻塞,拿到锁则返回0,否则返回错误编号EBUSY
1.4 pthread_mutex_unlock解锁
int pthread_mutex_unlock(pthread mutex_t *mutex);
上锁后,一定要在合适时期进行解锁
1.5 pthread_mutex_timedlock超时锁
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict tsptr);
//返回值:若成功,返回0;否则,返回错误编号
和普通互斥锁的区别是超时锁会有一个最大的超时时间,不会发生死锁的情况
timespec结构体为绝对时间
clock_gettime(CLOCK_REALTIME)获取当前时间,然后秒钟+10,则代表一个10S超时锁。
超时到期,返回ETIMEDOUT
1.6关于死锁问题的血泪总结
这里都是一些个人血和泪的经验~
1)锁的粒度太大,容易出现很多线程阻塞等待锁的情况,导致程序并发性差
而如果锁的粒度太细,过度的锁开销会使系统性能受到影响,代码变的复杂,所以要找到一个平衡
2)在程序开发中,写完lock之后,就要条件反射一般,将unlock写好,防止之后忘记
3)在函数内进行lock之后,一定要谨防函数直接return的情况,return 之前要释放锁,否则下次拿锁会出现死锁的情况。
4)带锁的线程,在使用pthread_cancle强制取消线程时,要对锁进行处理,详情可见
https://blog.csdn.net/qq_43603125/article/details/136918805
5)两把锁一起使用时,切记要注意交叉锁的情况
线程A 拿到了线程锁A 并阻塞等待线程锁B
线程B 拿到了线程锁B 并阻塞等待线程锁A 造成了死锁。
所以如果在调用外部注册进来的回调函数,一定不要带锁去调用,因为不知道外部会如何操作
2.读写锁(rwlock)
读写锁与互斥量类似,但允许更高的并行性,非常适合对数据结构读的次数远大于写的情况。
读写锁有三种状态:读模式下加锁状态、写模式下加锁状态、不加锁状态。
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
写加锁状态:所有读、写的线程都会阻塞
读加锁状态:所有读操作都能拿到锁,写的线程都会阻塞,直到所有线程释放读锁。当写线程试图拿锁时,锁会阻塞之后的读模式锁请求,可以避免读模式锁长期占用,写锁一直拿不到。
2.1 pthread_rwlock_init 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
//成功返回0,失败返回错误编号
NULL 代表默认属性
使用读写锁和互斥锁基本一样,首先便要初始化
2.2 pthread_rwlock_destroy 销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//两个函数的返回值:若成功,返回0:否则,返回错误编号
不使用读写锁之后需要销毁
2.3 上锁操作
int pthread_rwlock_rdlock(pthread_rwlock_t * rwlock); //上读锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); //上写锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t * rwlock); //尝试上读锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); //尝试上写锁
int pthread_rwlock_timedrdlock(pthread rwlock t *restrict rwlock,
const struct timespec *restrict tsptr); // 上读锁的超时版本
int pthread rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,
const struct timespec*restrict tsptr); // 上写锁的超时版本
//所有函数的返回值:若成功,返回0;否则,返回错误编号
和互斥锁一致,读写锁同样有普通、尝试、超时的三个版本。超时到期,返回ETIMEDOUT。
根据业务需求来选择合适的锁
2.4 解锁操作
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
//函数的返回值:若成功,返回0;否则,返回错误编号
3.条件变量(cond)
条件变量允许一个线程等待另一个线程满足某个条件后再继续执行,避免线程无谓的轮询和忙等待,提高了系统的响应能力和效率,为了避免竞争,需要搭配互斥锁一起使用。
主要包括两个动作:1)A线程等待条件变量成立而挂起 2)B线程使条件成立 <给出条件成立信号>
3.1 pthread_cond_init初始化条件变量
//静态初始化
static cond = PTHREAD_COND_INITALIZER;
//动态初始化
pthread_cond_t cond;
pthread_cond_init(&cond;)
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
3.2 pthread_cond_destroy反初始化条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
//两个函数的返回值:若成功,返回0:否则,返回错误编号
3.3 pthread_cond_wait等待条件变量
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:否则,返回错误编号
带超时版本:超时到期条件未出现,pthread_cond_timewait将重新获取互斥量,然后返回错误ETIMEDOUT。
3.4 pthread_cond_signal唤醒线程
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
// 两个函数的返回值:若成功,返回0:否则,返回错误编号
signal至少唤醒一个等待该条件的线程
broadcast 唤醒等待该条件的所有线程
3.5 关于条件变量的血泪总结
这些都是实践+理论得到的血汗经验~,学到了赶紧给我点个收藏
当线程A调用pthread_cond_wait 函数,自动把线程A放到等待条件的线程列表上。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
上述代码中是以前写的,现在回顾看起来有一些小问题,应该把pthread_cond_signal放到lock和unlock中间。
这里会涉及到一个虚假唤醒的问题,假设多线程等待同一个条件变量,而信号发生的地方只有一个,那么在信号发生后,可能会同时唤醒多个线程,所以上面的代码pthread_cond_wait醒来后,再次对workq进行了检查,防止虚假唤醒!
4.自旋锁(spin)
自旋锁和互斥锁类似
**自旋锁是一种非阻塞锁:**需要拿锁时,CPU会不停的去轮询,不停的尝试获取自旋锁。适用于占用时间短的情况,节省线程调度的时间成本。
**互斥锁是一种阻塞锁:**需要拿锁时,若无法获取到锁,线程会被挂起,当其他线程释放互斥量,操作系统会激活被挂起的线程。
4.1 pthread_spin_init初始化
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
和互斥锁、读写锁一样,使用之前都需要进行初始化
4.2 pthread_spin_destroy销毁
int pthread_spin_destroy(pthread_spinlock_t *lock);
4.3 pthread_spin_lock上锁
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
普通上锁以及尝试上锁版,若尝试发现无法上锁,则直接返回错误编号EBUSY
4.4 pthread_spin_unlock解锁
int pthread_spin_unlock(pthread_spinlock_t *lock);
//所有函数的返回值:若成功,返回0:否则,返回错误编号
5.屏障(barrier)
屏障:用户协调多个线程并行工作的同步机制
简单来说,屏障允许每个线程等待,直到所有的合作线程都到达某一点,再统一执行某个任务,适用于高并发的情况
如果只用一个线程进行800万个数的排序,非常耗性能
但在8核处理器系统上,分成8个线程去做,每个线程处理完毕都wait 等待其他线程
最后再到一个线程上进行合并,速度能提升好几倍!
5.1 pthread_barrier_init初始化
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
const pthread_barrierattr_t *restrict attr,
signed int count);
// 成功返回0或PTHREAD_BARRIER_SERIAL_WAIT(说明是主线程),失败返回错误编号
count:需要等待的个数,当pthread_barrier_wait的调用个数到达count后,所有线程都将被唤醒
attr:屏障属性
5.2 pthread_barrier_destroy 反初始化
int pthread_barrier_destroy(pthread_barrier_t *barrier)
//返回值:若成功,返回0;否则,返回错误编号
5.3 pthread_barrier_wait到达屏障
int pthread_barrier_wait(pthread_barrier_t *barrier);
//返回值:若成功,返回0,否则,返回错误编号
调用后线程将阻塞,表明已经到达屏障,正在等待其他线程,调用这个函数后,wait_count–,从init设定的wait_count减到0时,则所有线程被唤醒。
屏障的使用是可以复用的,也就是说当一次所有线程都被唤醒后,之后可以继续使用这个屏障,计数重新来到init的count;