Linux多线程(2)-线程间同步的5种方式,一次性说清楚!

线程为什么要同步?

当进程中的多个线程,同时读取一块内存数据,与此同时其中一个或多个线程修改了这块内存数据。这样就会导致不可预期的结果。
因为线程不安全引起的错误往往非常难发现,不能稳定复现,所以在编码的时候就应该事先考虑,会不会有多线程并发的情况,就像我们写完malloc之后就会去判空,然后再memset一样,多线程操作环境,就应该选择互斥锁或其他方式,保护共享资源

问题示意图:
image.png

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放到等待条件的线程列表上。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
image.png
上述代码中是以前写的,现在回顾看起来有一些小问题,应该把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;

  • 23
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux驱动中的多线程指的是驱动程序中同时执行的多个线程多线程的产生有几个原因:多线程并发访问、抢占式并发访问、中断程序并发访问和SMP(多核)核并发访问。 在编写Linux驱动程序时,我们需要考虑并发和竞争访问问题。并发访问会带来竞争问题,特别是在涉及共享数据段的临界区,我们必须保证一次只有一个线程访问临界区,也就是要保证临界区是原子访问的。这意味着一个访问操作必须是一个完整的步骤,不能再进行拆分。 为了避免并发和竞争访问,在编写驱动程序时,我们应该尽早考虑并发和竞争问题,而不是在编写完驱动程序后再处理这些问题。这样可以避免在调试过程中出现难以查找的问题,并提高驱动程序的可靠性。 在Linux驱动程序中,可以使用多方法来处理多线程问题。其中一常见的方法是使用互斥锁(mutex)来保护临界区,只允许一个线程访问临界区,其他线程需要等待。另一方法是使用信号量(semaphore),允许多个线程同时访问临界区,但是需要控制访问的数量。还可以使用读写锁(rwlock)来实现对临界区的读写操作的并发访问控制。 在编写驱动程序时,我们还应该注意避免死锁的问题。死锁是指多个线程互相等待对方释放资源导致无法继续执行的情况。为了避免死锁,我们需要合理地设计锁的获取和释放顺序,并避免循环依赖。 总结起来,Linux驱动中的多线程是为了实现并发访问和提高系统性能。在编写驱动程序时,我们需要考虑并发和竞争访问问题,并使用适当的同步机制来保护临界区,避免死锁的发生[3]。 引用自:一, linux系统并发产生的原因和竞争问题. (n.d.). Retrieved from https://www.jianshu.com/p/05f3b8f41e2f 引用自:二, linux系统竞态问题描述. (n.d.). Retrieved from https://www.jianshu.com/p/eb1f9f0b5f16

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值