自旋锁
首先通过一个例子来带着大家理解自旋锁,在生活中大家肯定都等过人比如你们一家人准备出去玩可是出发的时候妻子发现自己还没有化妆于是连忙赶回了家这个时候其他人就得在楼下等着,但是这个等又分为两种情况第一种是真的在楼下等其他的什么事都没有干,那么这种情况就一定非常的枯燥无聊,所以你会隔三差五的打电话问你的妻子好了没,下楼了没,化妆化完了没,第二种情况就是老婆在家里化妆但是你没有在楼下等,你干其他的事情去了,你跟妻子说画完状之后跟我打个电话我立马回到楼下来接你,因为你干其他的事情去了所以不会干其他的事情也就不会不停的打电话来催你老婆了。那么这是生活上的两种情况,程序中的锁也是相同的场景,我们之前用锁的时候会发现当申请不到锁的时候该线程就会挂起等待,那么这就是上面等人的第二个场景,而我们待会要介绍的自旋锁就是上面的第一个场景,当没有申请到锁资源的时候自旋锁就会不停的访问锁好了没就绪了没直到申请到了为止,那么这里就有一个问题:是什么决定了等待的方式?因为已经被线程访问的临界资源决定了其他线程要进行等待,所以一个成功申请临界资源的线程在临界区呆的多长时间决定了等待的方式,如果等待的时间较长就选择挂起等待锁也就是我们之前用的锁,如果等待的时间较短就使用自旋锁,那如何判断时间的长短呢?答案是没有办法判断的这个是一个相对的东西,是没有绝对的判断标准的可以通过两者都用的方式测试一下效率来做最后的判断,但是自旋锁我们是不推荐使用的,因为挂起等待虽然看上去会慢点但是一旦自旋锁评估失误,他会大量的消耗资源比如说挂起等待的时候出现死锁了不起就是两者都挂起等待,但是自旋锁一旦出现了类似于死锁的情况就会不停的检测锁的状态但是却没有一个人释放锁会造成大量的资源浪费,所以自旋锁很危险不建议使用。
自旋锁的接口
自旋锁的接口和挂起等待锁的接口十分的相似,首先来看一个名为pthread_spin_lock函数
可以看到这个函数需要一个pthread_spinlock_t类型的指针,那么这个pthread_spinlock_t就是上面说的自旋锁,在使用自旋锁之前先定义一个自旋锁变量,然后再使用pthread_spin_init函数对其进行初始化:
第一个参数表示对哪把锁初始化,第二个参数直接传递0就行,当不需要用这个锁对象之后就可以使用pthread_spin_destroy对其进行销毁,当不需要用这把锁的时候就可以使用pthread_spin_unlock将其解锁,当需要这把锁进行枷锁的时候就可以使用函数pthread_spin_lock函数:
当申请锁失败之后得的动作需要我们自己做的话就可以使用pthread_spin_trylock函数,如果不想自己做的话就使用函数pthread_spin_lock函数,那么这就是自旋锁的接口。
读者写着问题
在编写多线程的时候有一种情况是十分常见的。那就是有些公共数据修改的机会比较少,相比较改写它们读的机会反而高的多。通常而言在读的过程中往往伴随着查找的操作,中间耗时很长。给这种代码段加锁会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有那就是读写锁,我们可以把线程分为两类一个是读者线程一个写者线程,在处理读者写着问题的过程中我们也得维护3 2 1原则,也就是3种关系读者和读者的关系,写者和写者的关系,读者和写者的关系,写者和写者之间一定是互斥的关系因为两个线程在写的时候可能会同一块空间进行写入,写者和读者之间也是互斥关系,因为写者还没有写完的时候读者就跑过来读取,那么这个时候读取的数据肯定是不完整的,并且写者写完数据之后可能接连着就对数据进行修改读者还来不及进行读取,所以写者和读者之间还有个同步的关系,因为读者不会对数据进行修改,多个读者在读取数据的时候也不会出现线程安全问题就好比大家上课看老师ppt一样每个人都能读,所以读者和读者之间没有关系,2就表示两个角色也就是读者和写着,1就表示的是共享资源,那这里就有一个问题这里的读者和写着模型跟消费者生产者消费者之间有什么区别呢?答案就是消费者会拿走数据,一个消费者线程拿走数据之后其他消费者是看不到这个数据的,但是读者不一样读者只会拷贝不会拿走,所以读者和读者之间没有关系,所以读者写着模型适合于:一次发布,很长时间不会修改,大部分都是被读取的场景,比如说新闻,报纸,小说等等都是少写多读的场景。
读者写着的接口
首先来看函数pthread_rwlock_init函数,这个函数也跟之前学的mutex锁函数类似:
首先使用读写锁之前也得创建一个读写锁对象也就是pthread_rwlock_t对象,然后就可以使用pthread_rwlock_init对这个对象进行初始化,当不再使用这个对象之后就可以使用pthread_rwlock_destory函数将对象销毁,在枷锁的时候如果你是读者就使用pthread_rwlock_rdlock函数进行枷锁
如果你是写着线程你就使用pthread_rwlock_wrlock函数对其进行枷锁:
不管你是写着还是读者,当你想要解锁的时候都可以使用函数pthread_rwlock_unlock
如果当前没有枷锁,那么读者和写者都能正常的申请到锁,如果当前写者申请到了锁那么其他的写者线程和读者线程就申请不到锁,如果读者线程申请到了锁那么其他的读者线程也能申请到锁,但是写者线程是申请不到锁,我们可以通过下面的图片来了解:
读写锁的原理
这里我们用伪代码的方式来带着大家理解锁的原理,首先对于写锁我们可以认为里面就一个挂起等待锁:
//写着锁
pthread_mutex_t wrlock
当写锁要枷锁的时候就直接对挂起锁进行枷锁,解锁的时候也是直接对挂起锁进行解锁就行
//写着锁枷锁
lock(&wrlock);
//写着锁解锁
unlock(&wrlock);
而读者锁就不一样,我们可以将他的内部看成一把挂起锁和一个计数器
//读者锁
pthread_mutex_t rdlock;
int read_count=0;
因为有计数器的存在读者锁在挂起的时候会先对自己的挂起锁进行枷锁,然后再对计数器进行加一
lock(&rdlock);
read_count++;
然后我们就判断一下当前计数器的值是否为1,如果为1的话就表明当前是第一次申请读者锁得防止写者往共享区中写数据,所以得将写着锁中的wrlock锁起来:
lock(&rdlock);
read_count++;
if(read_count==1)
{lock(&wrlock)}
将写者锁枷锁起来后就可以将自己的rdlock进行解锁然后对共享数据进行访问
lock(&rdlock);
read_count++;
if(read_count==1)
{lock(&wrlock)}
unlock(&rdlock)
//读取数据
在解锁的时候就是先对rdlock进行枷锁然后对计数器的值进行减一,如果计数器的值为0了就表明一个读者都没有了,那么这个时候就可以将写者内部的锁进行解锁最后将rdlock进行解锁即可:
//读者锁解锁
lock(&rdlock)
read_count--;
if(read_count==0)
{unlock(&wrlock)}
unlock(&rdlock);
那么这就是读者写着锁内部实现的伪代码,当读者先运行的时候会对wrlock进行枷锁这样就导致读者在枷锁的时候被挂起等待,当写者先运行的时候就会导致第一个运行起来的读者无法申请到锁从而被挂起等待,这样就做到了读者和写者之间的互斥,那么这就是读写锁的大致实现。