填料厂家
免费服务热线

Free service

hotline

010-00000000
填料厂家
热门搜索:
成功案例
当前位置:首页 > 成功案例

看一看:Libtask源码解析之锁

发布时间:2022-03-31 17:32:47 阅读: 来源:填料厂家
Libtask源码解析之锁 作者: theanarkh 2021-02-20 06:09:46 开发 前端 libtask中其实不需要锁,因为libtask中协程是非抢占式的,不存在竞态条件。但是libtask还是实现了一套锁的机制。我们看一下这个锁机制的实现。首先我们看一下结构体。

本文转载自微信公众号「编程杂技」,作者theanarkh 。转载本文请联系编程杂技公众号。

libtask中其实不需要锁,因为libtask中协程是非抢占式的,不存在竞态条件。但是libtask还是实现了一套锁的机制。我们看一下这个锁机制的实现。首先我们看一下结构体。

  1. structQLock
  2. {
  3. //锁持有者
  4. Task*owner;
  5. //等待该锁的队列
  6. Tasklistwaiting;
  7. };

接着我们看一下锁的操作。

加锁

  1. staticint_qlock(QLock*l,intblock)
  2. {
  3. //锁没有持有者,则置当前协程为持有者道路建设房屋拆迁补偿怎么补,直接返回,1表示加锁成功
  4. if(l->owner==nil){
  5. l->owner=taskrunning;
  6. return1;
  7. }
  8. //非阻塞,则直接返回,0表示加锁失败
  9. if(!block)
  10. return0;
  11. //插入等待锁队列
  12. addtask(&l->waiting,taskrunning);
  13. taskstate("qlock");
  14. //切换到其他协程
  15. taskswitch();
  16. //切换回来时,如果持有锁的协程不是当前协程,则异常退出,因为只有持有锁才会被切换回来,见unqlock
  17. if(l->owner!=taskrunning){
  18. fprint(2,"qlock:owner=%pself=%poops\n",l->owner,taskrunning);
  19. abort();
  20. }
  21. return1;
  22. }

如果当前锁没有持有者,则当前协程X就变成锁的持有者,否则把协程X插入等待锁队列中,然后让出cpu,切换到其他协程。当后续锁被释放并被协程X持有时,协程X就会被唤醒继续持续。加锁可以分为阻塞和非阻塞两种模式。非阻塞就是加锁失败也不会切换协程。

  1. //阻塞式加锁
  2. voidqlock(QLock*l)
  3. {
  4. _qlock(l,1);
  5. }
  6. //非阻塞式加锁
  7. int
  8. canqlock(QLock*l)
  9. {
  10. return_qlock(l,0);
  11. }

释放锁

接下来我们看一下释放锁的逻辑

  1. //释放锁
  2. voidqunlock(QLock*l)
  3. {
  4. Task*ready;
  5. //锁并没有持有者,异常退出
  6. if(l->owner==0){
  7. fprint(2,"qunlock:owner=0\n");
  8. abort();
  9. }
  10. //如果还有协程在等待该锁,则置为持有者,并且从等待队列中删除,然后修改状态为就绪并加入就绪队列
  11. if((l->owner=ready=l->waiting.head)!=nil){
  12. deltask(&l->waiting,ready);
  13. taskready(ready);
  14. }
  15. }

当锁被释放时拆迁厂房多高算两层,如果还有协程在等待该锁,则从等待队列中摘取一个节点,然后变成锁的持有者并从等待队列中删除。最后插入就绪队列等待调度。以上是一种互斥锁的实现。下面我们再来看一下读写锁机制,读写锁也是互斥的,但是在某些情况下也可以共享。我们看一下读写锁的数据结构。

  1. structRWLock
  2. {
  3. //正在读的读者个数
  4. intreaders;
  5. //当前正在写的写者,只有一个
  6. Task*writer;
  7. //等待读和写的队列
  8. Tasklistrwaiting;
  9. Tasklistwwaiting;
  10. };

接着我看一下加锁逻辑。

加读锁

  1. //加读锁
  2. staticint_rlock(RWLock*l,intblock)
  3. {
  4. /*
  5. 没有正在写并且没有等待写,则加锁成功,并且读者数加一
  6. */
  7. if(l->writer==nil&&l->wwaiting.head==nil){
  8. l->readers++;
  9. return1;
  10. }
  11. //非阻塞则直接返回
  12. if(!block)
  13. return0;
  14. //插入等待读队列
  15. addtask(&l->rwaiting,taskrunning);
  16. taskstate("rlock");
  17. //切换上下文
  18. taskswitch();
  19. //切换回来了,说明加锁成功
  20. return1;
  21. }

当且仅当没有正在写的写者和等待写的写者时,才能加读锁成功,否则根据加锁模式进行下一步处理,直接返回加锁失败或者插入等待队列,然后切换到其他协程。我们看到当有一个等待写的协程时(l->wwaiting.head != nil),则后续的读者就无法加锁成功,而是被插入等待队列,否则可能会引起写者饥饿。

加写锁

  1. //加写锁
  2. staticint_wlock(RWLock*l,intblock)
  3. {
  4. //没有正在写并且没有正在读,则加锁成功,并置写者为当前协程
  5. if(l->writer==nil&&l->readers==0){
  6. l->writer=taskrunning;
  7. return1;
  8. }
  9. //非阻塞则直接返回
  10. if(!block)
  11. return0;
  12. //加入等待写队列
  13. addtask(&l->wwaiting,taskrunning);
  14. taskstate("wlock");
  15. //切换
  16. taskswitch();
  17. //切换回来说明拿到锁了
  18. return1;
  19. }

当且仅当没有正在写的写者和没有正在读的读者时,才能加写锁成功。否则类似加读锁一样处理。

释放读锁

  1. //释放读锁
  2. voidrunlock(RWLock*l)
  3. {
  4. Task*t;
  5. //读者减一,如果等于0并且有等待写的协程,则队列第一个协程持有该锁
  6. if(--l->readers==0&&(t=l->wwaiting.head)!=nil){
  7. deltask(&l->wwaiting,t);
  8. l->writer=t;
  9. taskready(t);
  10. }
  11. }

持有读锁,说明当前肯定没有正在写的写者,但是可能有等待写的写者和等待读的读者(因为有等待写的写者导致无法加锁成功)。当释放读锁时,如果还有其他读者,则其他读者可以继续持有锁,因为读者可以共享读锁,而写者保持原来状态。如果这时候没有读者但是有等待写的写者,则从队列中选择第一个节点成为锁的持有者,其他的写者则继续等待,因为写者不能共享写锁。

释放写锁

  1. //释放写锁
  2. voidwunlock(RWLock*l)
  3. {
  4. Task*t;
  5. //没有正在写,异常退出
  6. if(l->writer==nil){
  7. fprint(2,"wunlock:notlocked\n");
  8. abort();
  9. }
  10. //置空,没有协程正在写
  11. l->writer=nil;
  12. //有正在读,异常退出,写的时候,是无法读的
  13. if(l->readers!=0){
  14. fprint(2,"wunlock:readers\n");
  15. abort();
  16. }
  17. //释放写锁时,优先让读者持有锁,因为读者可以共享持有锁,提高并发
  18. //读可以共享,把等待读的协程都加入就绪队列,并持有锁
  19. while((t=l->rwaiting.head)!=nil){
  20. deltask(&l->rwaiting,t);
  21. l->readers++;
  22. taskready(t);
  23. }
  24. //释放写锁时,如果又没有读者,并且有等待写的协程,则队列的第一个等待写的协程持有锁
  25. if(l->readers==0&&(t=l->wwaiting.head)!=nil){
  26. deltask(&l->wwaiting,t);
  27. l->writer=t;
  28. taskready(t);
  29. }
  30. }

持有写锁,可能有等待写的写者和等待读的读者。这里是读者优先持有锁,因为读者可以共享持有锁,提高并发,如果没有读者,则再判断写者。

总结:单纯的互斥锁是比较简单的,读写锁就相对复杂一点,主要是要根据读锁和写锁的特性制定一些策略,比如避免饥饿问题。libtask的方式是,加写锁的时候,当无法持有锁的时候,申请者就会被插入等待等待队列。这个是没有什么好说的,加读者的时候,情况就复杂了点,如果这时候有读者正在持有锁,理论上,申请者也可以持有锁,因为读锁是共享的,但是单纯这样处理的话,可能会导致等待写的写者一直拿不到锁,所以这里需要判断是否有等待写的写者,如果有则当前申请者则不能再持有读锁,而是要加入等待队列。那么在释放锁的时候,当释放读锁时,优先让等待写的写者持有锁,再到等待读的读者持有锁。同样,当释放写锁时,优先让读者持有锁,这样就能比较好地平衡读者和写者持有锁的机会。