背景问题:在特定的应用场景下,多线程不进行同步会造成什么问题?
通过多线程模拟多窗口售票为例:
分析:总票数只有20张,却卖出了23张,是非常明显的超买超卖问题,而造成这个问题的根本原因就是同时发生的各个线程都可以对ticket_sum进行读取和写入!
ps:
1.在并发情况下,指令执行的先后顺序由内核决定,同一个线程内部,指令按照先后顺序执行,但不同线程之间的指令很难说清楚是哪一个先执行,如果运行的结果依赖于不同线程执行的先后的话,那么就会形成竞争条件,在这样的情况下,计算的结果很难预知,所以应该尽量避免竞争条件的形成
2.最常见的解决竞争条件的方法是将原先分离的两个指令构成一个不可分割的原子操作,而其他任务不能插入到原子操作中!
3.对多线程来说,同步指的是在一定时间内只允许某一个线程访问某个资源,而在此时间内,不允许其他线程访问该资源!
4.线程同步的常见方法:互斥锁,条件变量,读写锁,信号量
一.互斥锁
本质就是一个特殊的全局变量,拥有lock和unlock两种状态,unlock的互斥锁可以由某个线程获得,一旦获得,这个互斥锁会锁上变成lock状态,此后只有该线程由权力打开该锁,其他线程想要获得互斥锁,必须得到互斥锁再次被打开之后
采用互斥锁来同步资源:
分析:通过为售票的核心代码段加互斥锁使得其变成了一个原子性操作!不会被其他线程影响
1.互斥锁的初始化
互斥锁的初始化分为静态初始化和动态初始化
静态:pthread_mutex_tmutex_x=PTHREAD_MUTEX_INITIALIZER;//staticinitmutex
动态:pthread_mutex_init函数
ps:互斥锁静态初始化和动态初始化的区别?
待补充。。。。
2.互斥锁的相关属性及分类
attr表示互斥锁的属性
pshared表示互斥锁的共享属性,由两种取值:
1)PTHREAD_PROCESS_PRIVATE:锁只能用于一个进程内部的两个线程进行互斥(默认情况)
2)PTHREAD_PROCESS_SHARED:锁可用于两个不同进程中的线程进行互斥,使用时还需要在进程共享内存中分配互斥锁,然后为该互斥锁指定属性就可以了
互斥锁的分类:
参数type表示互斥锁的类型,总共有以下四种类型:
1.PTHREAD_MUTEX_NOMAL:标准互斥锁,第一次上锁成功,第二次上锁会失败并阻塞
2.PTHREAD_MUTEX_RECURSIVE:递归互斥锁,第一次上锁成功,第二次上锁还是会成功,可以理解为内部有一个计数器,每加一次锁计数器加1,解锁减1
3.PTHREAD_MUTEX_ERRORCHECK:检查互斥锁,第一次上锁会成功,第二次上锁出错返回错误信息,不会阻塞
4.PTHREAD_MUTEX_DEFAULT:默认互斥锁,第一次上锁会成功,第二次上锁会失败
3,测试加锁函数
intpthread_mutex_lock(mutex):测试加锁函数在锁已经被占据时返回EBUSY而不是挂起等待,当然,如果锁没有被占领的话可以获得锁
为了清楚的看到两个线程争用资源的情况,我们使得其中一个函数使用测试加锁函数进行加锁,而另外一个使用正常的加锁函数进行加锁
分析:线程1不满足条件被阻塞,然后线程2运行,改变了条件,线程2发行条件改变了通知线程1运行,然线程1不满足条件被阻塞,然后线程2运行,改变了条件,线程2发行条件改变了通知线程1运行,然后线程2结束,然后线程1继续运行,然后线程1结束,为了确保线程1先执行,在创建线程2之前我们sleep了2秒
ps:
1.条件变量通过运行线程阻塞和等待另一个线程发送信号的方法弥补互斥锁的不足,常常和互斥锁一起使用,使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开响应的互斥锁并等待条件发生变化,一旦其他的某个线程改变了条件变量,它将通知响应的条件变量换线一个或多个正被此条件变量阻塞的线程,这些线程将重新锁定互斥锁并且重新测试条件是否满足
1.条件变量的相关函数
1)创建
静态方式:pthread_cond_tcondPTHREAD_COND_INITIALIZER
动态方式:intpthread_cond_init(cond,NULL)
Linuxthread实现的条件变量不支持属性,所以NULL(cond_attr参数)
2)注销
intpthread_cond_destory(cond)
只有没有线程在该条件变量上,该条件变量才能注销,否则返回EBUSY
因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程!(请参考条件变量的底层实现)
3)等待
条件等待:intpthread_cond_wait(cond,mutex)
计时等待:intpthread_cond_timewait(cond,mutex,time)
1.其中计时等待如果在给定时刻前条件没有被满足,则返回ETIMEOUT,结束等待
2.无论那种等待方式,都必须有一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait形成竞争条件!
3.在调用pthread_cond_wait前必须由本线程加锁
4)激发
激发一个等待线程:pthread_cond_signal(cond)
激发所有等待线程:pthread_cond_broadcast(cond)
重要的是,pthread_cond_signal不会存在惊群效应,也就是是它最多给一个等待线程发信号,不会给所有线程发信号唤醒提他们,然后要求他们自己去争抢资源!
pthread_cond_signal会根据等待线程的优先级和等待时间来确定激发哪一个等待线程
下面看一个程序,找到程序存在的问题
三.读写锁
可以多个线程同时读,但是不能多个线程同时写
1.读写锁比互斥锁更加具有适用性和并行性
2.读写锁最适用于对数据结构的读操作读操作次数多余写操作次数的场合!
3.锁处于读模式时可以线程共享,而锁处于写模式时只能独占,所以读写锁又叫做共享-独占锁
4.读写锁有两种策略:强读同步和强写同步
在强读同步中,总是给读者更高的优先权,只要写者没有进行写操作,读者就可以获得访问权限
在强写同步中,总是给写者更高的优先权,读者只能等到所有正在等待或者执行的写者完成后才能进行读
不同的系统采用不同的策略,比如航班订票系统使用强写同步,图书馆查阅系统采用强读同步
根据不同的业务场景,采用不同的策略
1)初始化的销毁读写锁
静态初始化:pthread_rwlock_trwlock=PTHREAD_RWLOCK_INITIALIZER
动态初始化:intpthread_rwlock_init(rwlock,NULL),NULL代表读写锁采用默认属性
销毁读写锁:intpthread_rwlock_destory(rwlock)
在释放某个读写锁的资源之前,需要先通过pthread_rwlock_destory函数对读写锁进行清理。释放由pthread_rwlock_init函数分配的资源
如果你想要读写锁使用非默认属性,则attr不能为NULL,得给attr赋值
intpthread_rwlockattr_init(attr),给attr初始化
intpthread_rwlockattr_destory(attr),销毁attr
2)以写的方式获取锁,以读的方式获取锁,释放读写锁
intpthread_rwlock_rdlock(rwlock),以读的方式获取锁
intpthread_rwlock_wrlock(rwlock),以写的方式获取锁
intpthread_rwlock_unlock(rwlock),释放锁
上面两个获取锁的方式都是阻塞的函数,也就是说获取不到锁的话,调用线程不是立即返回,而是阻塞执行,在需要进行写操作的时候,这种阻塞式获取锁的方式是非常不好的,你想一下,我需要进行写操作,不但没有获取到锁,我还一直在这里等待,大大拖累效率
所以我们应该采用非阻塞的方式获取锁:
intpthread_rwlock_tryrdlock(rwlock)
intpthread_rwlock_trywrlock(rwlock)
读写锁的样例:
分析:信号量的值代表空闲的服务窗口,每个窗口一次只能服务一个人,有空闲窗口,开始服务前,信号量-1,服务完成后信号量+1
需要C/C++Linux服务器开发学习资料私信“资料”(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
版权声明:本站所有作品(图文、音视频)均由用户自行上传分享,仅供网友学习交流,不声明或保证其内容的正确性,如发现本站有涉嫌抄袭侵权/违法违规的内容。请举报,一经查实,本站将立刻删除。