Libtask源码解析之锁

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

[[382302]]

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

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

  1. struct QLock 
  2.     // 锁持有者 
  3.     Task    *owner; 
  4.     // 等待该锁的队列 
  5.     Tasklist waiting; 
  6. }; 

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

加锁

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

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

  1. // 阻塞式加锁 
  2. void qlock(QLock *l) 
  3.     _qlock(l, 1); 
  4.  
  5. // 非阻塞式加锁 
  6. int 
  7. canqlock(QLock *l) 
  8.     return _qlock(l, 0); 

释放锁

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

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

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

  1. struct RWLock 
  2.     // 正在读的读者个数 
  3.     int    readers; 
  4.     // 当前正在写的写者,只有一个 
  5.     Task    *writer; 
  6.     // 等待读和写的队列 
  7.     Tasklist rwaiting; 
  8.     Tasklist wwaiting; 
  9. }; 

接着我看一下加锁逻辑。

加读锁

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

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

加写锁

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

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

释放读锁

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

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

释放写锁

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

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

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

 

责任编辑:武晓燕 来源: 编程杂技
相关推荐

2021-02-19 06:56:33

架构协程应用

2021-07-02 08:51:09

Redisson分布式锁公平锁

2021-06-30 14:56:12

Redisson分布式公平锁

2022-12-07 08:02:43

Spring流程IOC

2022-02-14 14:47:11

SystemUIOpenHarmon鸿蒙

2021-07-01 09:42:08

Redisson分布式

2016-09-20 10:26:25

LaravelPHPMiddleware

2022-05-17 10:42:36

reboot源码解析

2016-09-20 10:15:49

LaravelPHPContainer

2022-08-08 08:03:44

MySQL数据库CBO

2021-07-03 17:45:57

分布式Redisson MultiLock

2022-06-13 14:18:39

电源管理子系统耗电量服务

2022-04-28 16:19:30

JS API 接口短信PDU

2020-09-10 09:05:29

UDP服务器Nodejs

2015-09-11 09:17:55

JavaJava HashMa

2021-09-16 15:08:08

鸿蒙HarmonyOS应用

2021-12-30 22:50:32

KafkaConsumer 源码

2011-06-23 15:32:05

Qt Windows消息

2021-09-09 06:55:43

AndroidViewDragHel原理

2022-07-05 16:03:29

电源管理子系统鸿蒙
点赞
收藏

51CTO技术栈公众号