当前位置: 首页 > 科技观察

Libtask源码分析锁

时间:2023-03-19 19:50:43 科技观察

本文转载自微信公众号《编程杂技》,作者theanarkh。转载本文请联系编程杂技公众号。libtask中其实不需要加锁,因为libtask中的协程是非抢占式的,不存在竞争条件。但是libtask还是实现了锁机制。我们来看看这个锁机制的实现。首先,让我们看一下结构。structQLock{//lockholderTask*owner;//queueTasklistwaiting等待锁;};然后我们看一下锁的操作。Lockstaticint_qlock(QLock*l,intblock){//锁没有持有者,则将当前协程设置为持有者直接返回,1表示加锁成功if(l->owner==nil){l->owner=taskrunning;return1;}//非阻塞,直接返回,0表示加锁失败if(!block)return0;//插入等待锁队列addtask(&l->waiting,taskrunning);taskstate("qlock");//切换到其他协程taskswitch();//切换回来时,如果持有锁的协程不是当前协程,会异常退出,因为只会切换回锁,见unqlockif(l->owner!=taskrunning){fprint(2,"qlock:owner=%pself=%poops\n",l->owner,taskrunning);abort();}return1;}如果当前锁没有持有者,则当前协程X成为锁的持有者,否则,将协程X插入等待锁队列,然后放弃cpu,切换到其他协程。当后续锁被协程X释放并持有时,协程X会被唤醒继续执行。加锁可以分为两种模式:阻塞和非阻塞。非阻塞是指如果锁失败,协程不会被切换。//阻塞锁voidqlock(QLock*l){_qlock(l,1);}//非阻塞锁intcanqlock(QLock*l){return_qlock(l,0);}释放锁来看看逻辑releasingthelock//释放锁voidqunlock(QLock*l){Task*ready;//锁无holder,异常退出if(l->owner==0){fprint(2,"qunlock:owner=0\n");abort();}//如果还有协程在等待锁,则将其设置为holder并从等待队列中删除,然后修改状态为就绪,加入就绪队列if((l->owner=ready=l->waiting.head)!=nil){deltask(&l->waiting,ready);taskready(ready);}}释放锁时,如果有协程在等待lock,然后从等待队列中挑选一个节点,然后成为锁持有者并将其从等待队列中删除。最后插入就绪队列等待调度。上面是一个互斥体的实现。接下来我们看一下读写锁机制。读写锁也是互斥的,但在某些情况下也可以共享。我们来看看读写锁的数据结构。structRWLock{//正在读intreaders的读者数;//当前正在写的writer,只有一个Task*writer;//等待读写的队列Tasklistrwaiting;任务列表等待;};然后我会看看锁定逻辑。加读锁//加读锁staticint_rlock(RWLock*l,intblock){/*如果不在写等待写,加锁成功,读者数加1*/if(l->writer==nil&&l->wwaiting.head==nil){l->readers++;return1;}//非阻塞直接返回if(!block)return0;//插入等待读队列addtask(&l->rwaiting,taskrunning);taskstate("rlock");//切换上下文taskswitch();//切换回来,表示加锁成功return1;}当且仅当没有正在写入和等待写入的写入者读锁可以加成功,否则根据加锁模式,进行下一步,直接返回加锁失败或者插入等待队列,然后切换到其他协程。我们看到,当有协程等待写入时(l->wwaiting.head!=nil),后续的reader将无法加锁成功,而是会被插入到等待队列中,否则可能会造成writerstarvation。添加写锁//添加写锁staticint_wlock(RWLock*l,intblock){//如果不是写也不是读,则加锁成功,writer为当前协程if(l->writer==nil&&l->readers==0){l->writer=taskrunning;return1;}//非阻塞,直接返回if(!block)return0;//加入等待队列addtask(&l->wwaiting,taskrunning);taskstate("wlock");//切换taskswitch();//切换回来表示已经获得锁return1;}当且仅当没有写者在写,也没有读者在读,写锁才可以添加成功。否则,就像加读锁一样处理。Releasethereadlock//释放读锁voidrunlock(RWLock*l){Task*t;//reader负一,若等于0且有协程等待写入,队列中第一个协程持有锁if(--l->readers==0&&(t=l->wwaiting.head)!=nil){deltask(&l->wwaiting,t);l->writer=t;taskready(t);}}持有readLock,说明当前肯定没有写者在写,但是可能有写者在等待写,读者在等待读(因为有写者在等待写,所以无法成功加锁)。当读锁被释放后,如果还有其他读者,其他读者可以继续持有锁,因为读者可以共享读锁,而写者保持原来的状态。如果没有读者但有写者在等待写,从队列中选择第一个节点成为锁持有者,其他写者将继续等待,因为写者不能共享写锁。Releasewritelock//释放写锁voidwunlock(RWLock*l){Task*t;//不写,异常退出if(l->writer==nil){fprint(2,"wunlock:notlocked\n");abort();}//空,没有协程在写l->writer=nil;//读,异常退出,写的时候,不可读if(l->readers!=0){fprint(2,"wunlock:readers\n");abort();}//释放写锁的时候,先让reader持有锁,因为reader可以共享锁提高并发//read可以共享,把等待的协程放到读取被添加到就绪队列并保持锁定while((t=l->rwaiting.head)!=nil){deltask(&l->rwaiting,t);l->readers++;taskready(t);}//释放写锁时,如果没有读者,有协程等待写,则队列中第一个等待写的协程持有锁if(l->readers==0&&(t=l->wwaiting.head)!=nil){deltask(&l->wwaiting,t);l->writer=t;taskready(t);}}持有一个写锁,可能有写者在等待写,也可能有读者在等待读。这里读者优先持有锁,因为读者可以共享持有锁来提高并发性。如果没有读者,那就评判作者。总结:简单的互斥锁比较简单,而读写锁比较复杂。主要是根据读锁和写锁的特点制定一些策略,比如避免饥饿问题。libtask的方法是在加写锁的时候,当锁不住的时候,申请者会被插入到等待队列中。对此无话可说。添加读者时,情况稍微复杂一些。如果此时有读者持有锁,理论上申请者也可以持有锁,因为读锁是共享的,只是简单这样处理的话,等待写入的写者可能一直都拿不到锁,所以这里需要判断是否有写者等待写。如果有,则当前申请者不能再持有读锁,必须加入等待队列。那么在释放锁的时候,释放读锁的时候,等待写的writer优先持有锁,然后等待读的reader持有锁。同样,在释放写锁时,优先让读者持有锁,这样可以更好地平衡读者和写者持有锁的机会。