问题一:互斥量是什么?
互斥量很简单,和操作系统中信号量实现互斥的机制一样,资源只有一个,同时只能有一个人去访问资源,一旦有人使用资源,其他人必须等待,等到使用者释放资源,其他人才可以使用资源。这个流程对应到linux下实现如下:
互斥量:pthread_mutex_t
方法:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restric 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);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
使用方法如下:
pthread_mutex_lock(mutex); 临界区; pthread_mutex_unlock(mutex);
pthread_mutex_trylock(mutex); 临界区; pthread_mutex_unlock(mutex);
问题二:当资源被某一个用户占用的时候,其他用户是阻塞等待还是非阻塞等待?那个更好呢?
linux下互斥量的实现考虑到等待进入临界区到底是阻塞等待还是非阻塞等待,我个人认为是非阻塞更好,因为我比较喜欢确定性,我觉得阻塞等待,无法知道资源到底什么时候释放?是否上一家在使用资源的时候遇到了什么恶心的问题,导致可能占用更长时间的资源?我宁愿先去打把dota,打完再看看是否可以使用资源。这就相当于让程序先干点其他的事情,待会再去看看是否可以进入临界区。
pthread_mutex_lock(mutex); //阻塞等待进入临界区 pthread_mutex_trylock(mutex); //非阻塞等待进入临界区,当尝试去访问资源的时候,如果资源未释放,直接返回错误编码。
问题三:有了互斥量作为同步方法为什么还需要读写锁?意义在哪里?
考虑这么一个场景,不同的线程去读一个临界区,既然不需要修改临界区的任何值,那么这段临界区相对于线程就是安全的,那么就可以让更多的线程进入临界区去读,而不是当一个线程进入临界区读的时候,其他线程都需要在外面等待。所以诞生了读写锁。
当一个线程已经在读,其他线程如果只是想读,可进入临界区。
当一个线程在写,其他线程如果想写/读,不可进入临界区。
当一个线程在读,其他线程想写/读,等该线程退出临界区后,优先让想写的线程进入临界区,以防止写线程无止境的等待。
可以说读写锁的诞生,提高linux下资源利用率。linux下读写锁的实现如下:
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t * restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t * rwlock);
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_unlock(pthread_rwlock_t * rwlock); //释放资源
使用方法如下:
int pthread_rwlock_rdlock(rwlock); //以阻塞读模式请求进入临界区 临界区; int pthread_rwlock_unlock(rwlock);
pthread_rwlock_wrlock(rwlock); //以阻塞写模式请求进入临界区 临界区; pthread_rwlock_unlock(rwlock);
pthread_rwlock_tryrdlock(rwlock); //以非阻塞读模式请求进入临界区 临界区; pthread_rwlock_unlock(rwlock);
pthread_rwlock_trywrlock(rwlock); //以非阻塞写模式请求进入临界区 临界区; pthread_rwlock_unlock(rwlock);
问题四:一个线程不管以读还是写的方式进入临界区,加锁/解锁的时候,如何区分到底是加读/解读还是加写/解写?
显然当多个线程以读的方式进入临界区,那么其中一个线程解锁,比不代表其他线程可写,只有所有进入临界区的读线程都退出临界区,才可能使写进程进入临界区。当写进程退出临界区,则任何其他进程都可以进入临界区。如何保证读写锁在加锁/解锁的时候,针对读/写采取不同的策略?
实现方案:
针对读/写采用两个计数值,当以读模式进入临界区,对read计数值++,当以写模式进入临界区,对write计数值++,read计数值可以>1,而write计数值只能有1和0两个值。
由于在进入临界区时,会记录线程到底是以读模式还是写模式进入临界区,所以退出临界区时,如果是读模式,则--;如果是写模式,也--;
以读模式进入临界区前,首先判断write计数值是否为1,为1,则等待,否则进入。以写模式进入临界区前,同样进行一系列判断。
问题五:当多个线程阻塞等待进入临界区,这些线程如何知道什么时候可以进入临界区,也就是什么时候其他线程释放了资源?
不管是互斥量还是读写锁,都是主动去询问,条件是否满足,是否可以进入临界区,由linux内核定时去询问条件是否满足,则内核定时的去执行一些定时任务,这些任务都是消耗资源,比如cpu,当然更好的办法肯定是被动获取条件是否满足,由占用资源的线程通知内核,我要释放资源了,然后内核通知等待线程准备好进入临界区。
两种不同方式实现等待进入临界区的方法:主动获取和被动通知。
问题六:当多个线程阻塞等待进入临界区,当条件满足了,由哪个等待线程最先获取进入临界区的权限呢?
我一直认为内核会为等待进入该临界区的线程分配一个等待队列(先进先出),谁最先阻塞,那就谁最先被唤醒。或者不以先进先出的方式,而是为每一个线程分配一个优先级,谁优先级最高,谁先被唤醒。
以上仅仅是我的想法,内核最终如何实现,还得看源码。
问题七:由问题五可知,等待进入临界区还有一种被动通知的方法,阻塞线程等待被唤醒,linux是否也实现了这个方案呢?
显然考虑到效率的问题,linux必然也实现了这个方案,那就是条件变量,pthread_cond_t。暂且先不看pthread_cond_t的实现方法,如果自己去实现一套通知的方案,该如何实现呢?
1.加锁
2.临界区
3.解锁并通知
实现上来说,内核维护一个等待队列,当某个线程退出临界区时,通知内核,然后由内核去唤醒等待队列上的线程。
乍一看,感觉上述方案应该就是linux下pthread_cond_t实现的方法,但是linux又如何实现呢?
pthread_cond_t //条件变量
int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t * cond);
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t * restric mutex); //阻塞等待进入临界区
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec * restrict timeout); //超时等待进入临界区,超时返回,相当于非阻塞
int pthread_cond_signal(pthread_cond_t * cond); //通知等待线程可以进入临界区
int pthread_cond_broadcast(pthread_cond_t * cond); //以广播的方式通知所有等待线程可以进入临界区
按我的理解由上述api,那么实现互斥访问的代码就应该如下:
pthread_cond_wait(pthread_cond_t,pthread_mutex_t); //阻塞 进入临界区; pthread_cond_signal(pthread_cond_t); //通知其他线程可以进入临界区
pthread_cond_timewait(pthread_cond_t,pthread_mutex_t); //超时返回,非阻塞 进入临界区; pthread_cond_signal(pthread_cond_t); //通知其他线程可以进入临界区
但是我发现网上描述的代码并不是这样子的,而是这样子:
pthread_mutex_lock (pthread_mutex_t); while(count==0) pthread_cond_wait(pthread_cond_t,pthread_mutex_t); count=count -1; pthread_mutex_unlock (pthread_mutex_t); 进入临界区; pthread_mutex_lock(pthread_mutex_t); if(count==0) pthread_cond_signal(pthread_cond_t); count=count+1; pthread_mutex_unlock(pthread_mutex_t);
什么原因导致代码量变得这么多呢?看到这里,我有几个疑问。
1.为什么在使用pthread_cond_wait要使用互斥量先加锁,后解锁?
2.为什么要给pthread_cond_wait()传入一个互斥量?
3.条件变量pthread_cond_t的作用又是什么?
4.为什么要引入count标志位?
要真正去理解上述三个问题,必须充分了解pthread_cond_wait()到底是如何实现的,我一直以为pthread_cond_wait()底层实现的时候,会有一个计数器来记录资源数,当一个线程请求进入临界区,如果没有其他线程已经进入临界区,那么该线程不会再pthread_cond_wait()处阻塞,而是直接返回,进入临界区。若已有线程进入临界区,则阻塞等待通知。
上述的是我一开始对pthread_cond_wait()的理解,但是我大错特错了,其实并不是这么回事,一旦一个线程去调用pthread_cond_wait()则马上阻塞,将线程插入等待队列中,并等待通知唤醒,而不是先去判断是否可以进入临界区,pthread_cond_wait()只有被通知的时候,才可以进入临界区。
除了对pthread_cond_wait()理解错了,对pthread_cond_signal()也理解错了,pthread_cond_signal()也不会对什么资源计数值进行++操作,而是直接去检查条件变量的阻塞列表中是否有线程等待,若有就唤醒它否则什么也不操作。
条件变量pthread_cond_t的作用是什么?假设有一个资源A,同时只能由一个线程使用,当某个线程调用pthread_cond_wait(),则立马将该线程加入等待资源A的等待队列中,那么该如何去标示这个等待队列,是不是得有一个标示符唯一的标示等待资源A的等待队列,当线程去调用pthread_cond_signal()时,立马给这个等待队列的线程发送唤醒信号。
i、为什么要使用互斥量先lock再unlock?
条件变量结构体pthread_cond_t, 他里面有一个等待列表,记录了所有因为此条件变量而产生阻塞的线程,显然这个等待列表是多个线程共享的,在使用的时候我们必须保证互斥的使用,所以就需要使用互斥量先上锁再解锁
ii、为什么要给wait函数传入互斥量
先讲一下wait函数的内部操作流程:在互斥的使用完等待列表(把线程加到等待列表中)后,就会让该调用线程阻塞,然后再执行一个解锁动作 ,等到线程被唤醒时,再执行一个加锁动作
下面解释wait函数内部的执行一次解锁和一次加锁的原因:
先执行一次解锁:这一步操作的时间是,把线程插入到条件变量的等待列表之后,让线程阻塞之前;设想如果此时没有解锁操作,那么阻塞线程会抱着锁睡着,这样的后果时,当再来一个线程想等待条件变量时,它会被阻塞在mutex.lock(),而不是被插入到条件变量的等待列表中,所以我们要先解锁,好让其他的线程可以顺利的调用wait函数
再执行一次加锁:这个就比较好解释了,因为此时系统又要互斥的操作等待列表即把阻塞线程从等待列表中删除,显然这要先加锁
为什么引入count标志位?
因为一个线程调用pthread_cond_wait()阻塞后,该线程很可能被其他的信号唤醒,而不是该条件变量的信号唤醒,比如说pthread_cond_broadcast()就可以唤醒所有调用pthread_cond_wait()阻塞的线程,所以需要必须添加一个标志位来表示条件变量的状态改变,也就是是否真的发送的signal信号。如果while循环测试,发现不是该条件变量的信号,则线程继续调用pthread_cond_wait()阻塞。
问题八:为什么会出现自旋锁?
不管是互斥量还是读写锁还是条件变量这些线程同步的机制,一旦临界区被其他线程占用,都会导致线程阻塞,线程一旦阻塞,必然产生cpu调度,让其他的线程获得处理器运行,由于上下文切换是非常耗cpu的,如果临界区非常短,不管任何一个线程占领临界区后,马上给就能释放临界区,那么由于短暂的时候,导致其他线程不能进入临界区而阻塞,产生上下文切换时非常不值的,所以希望有一种锁,该锁在占用后,任何想去获得该锁的线程都不会阻塞,而是出于一个忙等的状态,不释放cpu,自旋等待(不停的去循环测试能否获得锁,进入临界区),这就减少了上下文切换产生的消耗。相对于互斥量,自旋锁更适合自旋锁只适合与竞争不太激烈(即并发争锁的线程个数不多,why?因为一个线程在自旋的情况下去占用时间片其实是在浪费时间,还不如阻塞把时间片交给其他未阻塞的线程),并且临界区不大的情况。