多线程同步和竞争
同步:线程之间存在某种依存关系
竞争:多个线程对共享资源的非同步访问
1、计算机体系结构导致(原子操作)
2、程序使用过程中导致产生原因:
解决思路:需要同步的代码区称为“临界区”, 操作不可分割,我们称其为 “原子性”。
具体解决办法:同步(同步是一种更为复杂的互斥,而互斥是一种特殊的同步)
一、锁
互斥锁
互斥锁(又名互斥量)强调的是资源的访问互斥:互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这个资源。比如对全局变量的访问,有时要加锁,操作完了,在解锁。有的时候锁和信号量会同时使用的。也就是说,信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。而线程互斥量则是“锁住某一资源”的概念,在锁定期间内,其他线程无法对被保护的数据进行操作。在有些情况下两者可以互换。
/*有两种定义方式:
a.静态定义:pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
b.动态定义:先定义变量:pthread_mutex_t mutex;,然后调用pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr) 函数进行初始化,通常,互斥锁属性mutexattr为NULL,表示使用默认的属性,即:快速互斥锁。
*/
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);//动态分配互斥量,在释放内存前需要调用此函数接口,销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex); //若已被锁住,返回EBUSY,不阻塞,需要注意的是,只有确保在调用成功时,即返回值为0时,才能去解锁它。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *restrict tsptr);//绝对时间
妙用:在要保证互斥的数据中定义锁(细粒度锁),也要注意使用memset清零时不要把锁清零。
注意:递归锁只能在同一线程递归加锁
自旋锁
与互斥锁类似,但不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。
适用场景:锁被持有的时间短,线程不希望在重新调度上花费太多的成本。通常作为底层原语实现其他类型的锁;在非抢占式内核中非常有用,在用户层不是非常有用
int pthread_spin_init(pthread_spinlock_t *lock, int pshared); // 创建自旋锁
int pthread_spin_lock(pthread_spinlock_t *lock); // 加锁,阻塞
int pthread_spin_trylock(pthread_spinlock_t *lock); // 尝试加锁,非阻塞
int pthread_spin_unlock(pthread_spinlock_t *lock); // 解锁
和互斥锁的区别
1.实现方式上的区别:互斥锁是基于自旋锁实现的,所以自旋锁相较于互斥锁更加底层。
2.开销上的区别:获取不到互斥锁时会发生上下文切换并休眠,而自旋锁则“自旋”在原地直到被获取。
3.使用场景的区别:互斥锁只能在进(线)程中使用,不能在中断里使用,而自旋锁可以在中断里使用。
4.使用方式上区别:互斥锁只能由获取到该锁的进(线)程来释放,而自旋锁没有这个限制,但上锁和解锁一般都是成对使用的。
选用原则
根据上述区别可得出以下三条选用原则:
1.当需要保护的临界区较小时,宜选用自旋锁,否则选用互斥锁。因为当锁不能被获取到时,互斥锁的开销是上下文切换,上下文切换的开销是很大的。但当临界区执行时间的开销大于上下文切换的开销时,就适合使用互斥锁了,这种情况下使用自旋锁会让CPU空转直到其他执行单元解锁为止(还不如发生一次上下文切换),降低了系统效率。
2.自旋锁保护的临界区不能有引起切换上下文(休眠)的函数,但互斥锁可以。若自旋锁保护的临界区发生上下文切换,而切换上下文后执行的进(线)程又来获取该自旋锁,这样就必然会导致死锁的发生。另外,互斥锁保护的临界区也应该尽量避免阻塞(例如请求另外一个互斥锁),否则也容易出现死锁的情况。
3.如果被保护的临界区处于中断里,那么只能使用自旋锁。因为互斥锁可能会导致阻塞,而中断是不能被阻塞的。
读写锁(共享互斥锁)
读写锁可以有三种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。读写锁也叫做共享-独占锁,当读写锁以读模式锁住时,它是以共享模式锁住的,当它以写模式锁住时,它是以独占模式锁住的,读读共享,读写互斥。
pthread_rwlock_t rwlock=PTHREAD_RWLOCK_INITIALIZER;// 创建读写锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); // 加读锁,阻塞
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); // 加写锁,阻塞
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); // 释放读锁或者写锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); // 尝试加读锁,非阻塞
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); // 尝试加写锁,非阻塞
锁的常见问题
死锁
定义:线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
解决办法:
1、加锁顺序:按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁(并对这些锁做适当的排序),但总有些时候是无法预知的。
2、加锁时限
3、死锁检测
二、信号量
信号量不仅可以实现临界区的互斥访问控制,还可以线程间的事件同步。
优先级反转:高优先级任务被低优先级任务阻塞,导致高优先级任务迟迟得不到调度。但其他中等优先级的任务却能抢到CPU资源。-- 从现象上来看,好像是中优先级的任务比高优先级任务具有更高的优先权。
解决优先级反转的算法:
-
优先级继承:将低优先级任务的优先级提升到高优先级任务的级别,保证低优先级任务尽快释放资源
-
优先级天花板(冲顶协议):每个对象都有优先级冲顶值
分类需要补充
有名信号量:
可以用于不同进程间或多线程间的互斥与同步
无名信号量:
用于进程体内各线程间的互斥和同步,使用如下API(无名信号量,基于内存的信号量)
int sem_init (sem_t* sem, int pshared, unsigned int value);
int sem_wait (sem_t* sem);
int sem_trywait (sem_t* sem);
int sem_post (sem_t* sem);
int sem_getvalue (sem_t* sem);
int sem_destroy (sem_t* sem);
信号量强调的是线程(或进程)间的同步:“信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在sem_wait的时候,就阻塞在那里)。当信号量为单值信号量时,也可以完成一个资源的互斥访问。信号量测重于访问者对资源的有序访问,在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。缺点:无法传递数据数据
三、条件变量
在发送信号时,如果没有线程等待在该条件变量上,那么信号将丢失
int pthread_cond_init();//初始化方式和互斥锁差不所
int pthread_cond_destroy();//动态分配时,在释放内存前需要调用此函数接口。
pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);//在调用前要先获取锁,pthread_cond_wait函数执行时先自动释放指定的锁,然后等待条件变量的变化(即在阻塞时未锁住),在函数调用返回之前,自动将指定的互斥量重新锁住(然后操作变量)。
pthread_cond_timedwait();
int pthread_cond_signal(pthread_cond_t *cond);//通过条件变量cond发送消息,若多个消息在等待,它只唤醒一个,调用前后也得加锁
int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒等待该条件的所有线程
条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生,条件本身是由互斥量保护的。线程在改变条件状态前必须首先锁住互斥量,其它线程在获得互斥量之前不会察觉到这种改变,因此必须锁定互斥量以后才能计算条件。
为什么要使用互斥锁,将条件判断和wait变成一个原子操作
条件检测:
条件的检测是在互斥锁的保护下进行的。如果一个条件为假,调用condwaite线程阻塞,并释放等待状态改变的互斥锁。如果条件为真,操作条件为真可做的语句
改变条件
如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。
信号量和条件变量的不同
1.信号量内部使用了value,而条件变量更像是外部使用了value。两者的value语义也不同。
2.信号量自身wait和signal的原子操作保证了value的同步,条件变量只能多添加一个互斥量来实现value的同步。
3.信号量只能一次唤醒一个特定的进程,条件变量可以广播。
4.信号量内部定义value限制了其局限性,即只能对int类型资源的变化进行同步;条件变量实际上并不一定使用int类型value。while(P!=5),while(P!=NULL),while(a==b)等都可以作为一种条件.
5.如果用信号量,当wait(s)的时候,默认了“我使用s,并且根据情况判断是否等待s”。但如果用条件变量,就只有一个语义,就是“我等待s”,而并不必须使用s(我上面的例子,在后续是value--,这其实就是使用value。当然也可以是干一些别的。而如果用信号量,那么wait(s)里已经默认了value--)。这应该是最大的不同。
条件变量是基础元语,保留了底层实现高效化的可能性,可以用来实现读写锁,信号量,事件(win32 event),反过来却不行。信号量是个成品, 条件变量是个半成品, 灵活性更大一点。
条件变量典型应用场景
生产者消费者模型
生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
.
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。
要实现以下几点:
在缓冲区为空时,消费者不能再进行消费
在缓冲区为满时,生产者不能再进行生产
在一个线程进行生产或消费时,其余线程不能再进行生产或消费等操作,即保持线程间的同步
linux实现可阻塞的消息队列解决生产者消费者模型
Linux下的同个进程的线程通信没有RTOS下支持阻塞的消息队列。实现可阻塞的消息队列需要用到pthread中的条件变量(pthread_cond_t )和互斥锁(pthread_mutex_t )。
In Thread1:(消费者)
for(;;)
pthread_mutex_lock(&m_mutex);
while(判断条件) //一定要使用while,消费者线程可能有多个,如果使用广播
{
//等待过程中是解锁状态,线程2必须等到 mutex 被释放(也就是 pthread_cod_wait() 释放锁并进入wait_cond状态 ,此时线程2上锁) 的时候才能调用pthread_cond_signal,允许线程以无竞争的方式等待特定的条件发生。
pthread_cond_wait(&m_cond,&m_mutex); //多消费者的时候,同一时间只能返回一个返回,然后陆续去判断while中的判断条件,返回后立马加锁
}
/*其他对共享数据的操作 */
pthread_mutex_unlock(&m_mutex);
In Thread2:
pthread_mutex_lock(&m_mutex);
对条件和相关数据进行修改
pthread_cond_signal(&m_cond);
pthread_mutex_unlock(&m_mutex);
四、屏障
屏障(barrier)是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有的合作线程都到达某一点,然后所有线程都从该点继续执行。其实pthread_join函数也是一种屏障,允许一个线程等待,直到另一个线程退出。但屏障对象的概念更广,允许任意数量的线程阻塞,直到所有的线程完成处理工作,而线程不需要退出,当所有的线程达到屏障后被一同唤醒。
// 创建屏障
int pthread_barrier_init(pthread_barrier_t *barrier,const pthread_barrrierattr_t *attr,unsigned int count)
// 阻塞等待,直到所有线程都到达
int pthread_barrier_wait(pthread_barrier_t *barrier)// 创建屏障
int pthread_barrier_init(pthread_barrier_t *barrier,const pthread_barrrierattr_t *attr,unsigned int count)
// 阻塞等待,直到所有线程都到达
int pthread_barrier_wait(pthread_barrier_t *barrier)

824

被折叠的 条评论
为什么被折叠?



