当前位置: 首页 > web >正文

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;

http://www.xdnf.cn/news/11213.html

相关文章:

  • VMware虚拟机Windows 10安装使用教程(非常详细)从零基础入门到精通,看完这一篇就够了
  • 【Maven入门篇】(3)依赖配置,依赖传递,依赖范围,生命周期
  • 软件版本号扫盲——Beta RC Preview release等
  • latex如何输入正确的 双引号
  • WinForm中常用控件
  • C#中CheckListBox的用法
  • 搭建Serv-U FTP服务器共享文件外网远程访问「无公网IP」
  • 使用CImage类
  • Linux系统 虚拟机安装教程_虚拟机安装linux系统
  • 镜头选型——景深计算
  • 86年版五笔和98年版五笔区别
  • C语言从入门到精通保姆级教程(2021版上)
  • Response.AddHeader使用实例
  • functionexists php,PHP 检测函数是否被定义 function_exists 函数
  • [转载] Rss 与 Feed 的概念区别
  • 正则表达式匹配“不包含某些字符串”的技巧
  • SAPCRM销售订单集成创建
  • C#中Socket的简单使用
  • 注册系统热键 RegisterHotKey()
  • Android中ProgressDialog的使用
  • BP神经网络算法基本原理,bp神经网络算法详解
  • m3u8直播测试地址
  • 面向对象设计的八大基本原则
  • VMware虚拟机Windows 10安装使用教程(非常详细)从零基础入门到精通,看完这一篇就够了_vmware安装windows10
  • BUMO 区块链开发文档
  • Dogfooding-爱奇艺移动端后台灰度环境优化实践
  • Union和Union All的使用
  • jQuery.serializeArray() 函数详解
  • C/C++编程:log4cpp使用学习
  • wait,notify/notifyAll的使用及实现原理