本框架实现了一套协程同步原语来解决原生同步原语带来的阻塞问题,在协程同步原语之上实现更高层次的抽象——Channel用于协程之间的便捷通信,本文简单介绍一下如何设计。
我们都知道,一旦协程阻塞后整个协程所在的线程都将阻塞,这也就失去了协程的优势。编写协程程序时难免会对一些数据进行同步,而Linux下常见的同步原语互斥量、条件变量、信号量等基本都会堵塞整个线程,使用原生同步原语协程性能将大幅下降,甚至发生死锁的概率大大增加!
只有重新实现一套用户态协程同步原语才能解决这个问题。
在开始实现之前我们先简单介绍一下原理。原生同步对象由内核维护,当互斥量获取锁失败,条件变量wait,信号量wait获取失败时,内核将条件不满足的线程加入一个由内核维护的等待队列,然后阻塞线程,等待条件满足时将线程重新加入调度。
如同协程之于线程,我们很容易得到一个启示,既然内核维护等待队列会阻塞线程,那可不可以由用户态来维护等待队列呢。当获取协程同步对象失败时,用户将条件不满足的协程加入一个由用户维护的协程等待队列,然后让出协程,等待条件满足时将协程重新加入协程调度器调度。看,我们解决了线程同步问题,而且没有阻塞线程!
介绍完了原理,我们来看看实现,框架实现了一下以下几种协程同步原语
- CoMutex 协程锁
- CoCondvar 协程条件变量
- CoSemaphore 协程信号量
- Channel 消息通道
依赖关系如下:
CoMutex CoCondVar CoMutex CoCondVar
| | | |
----------- -----------
| |
V V
CoSemaphore Channel
为了保持通用性,我在部分代码采用了伪代码,你可以很容易地移植到你的协程框架上,当然如果你想看一下具体实现可翻到文章的最后找一下框架链接。
SpinLock 自旋锁
在此之前不得不提一下自旋锁。不管你是用TAS实现还是直接封装posix spin lock他们都有一个共同特点,就是不阻塞线程。我们的同步原语可以说都是基于自旋锁来实现,这里简单封装了一下posix自旋锁。
/**
* @brief 自旋锁
*/
class SpinLock : Noncopyable {
public:
using Lock = ScopedLock<SpinLock>;
SpinLock(){
pthread_spin_init(&m_mutex,0);
}
~SpinLock(){
pthread_spin_destroy(&m_mutex);
}
void lock(){
pthread_spin_lock(&m_mutex);
}
bool tryLock() {
return !pthread_spin_trylock(&m_mutex);
}
void unlock(){
pthread_spin_unlock(&m_mutex);
}
private:
pthread_spinlock_t m_mutex;
};
CoMutex 协程锁
CoMutex
的定义如下
/**
* @brief 协程锁
*/
class CoMutex : Noncopyable {
public:
using Lock = ScopedLock<CoMutex>;
bool tryLock();
void lock();
void unlock();
private:
// 协程所持有的锁
SpinLock m_mutex;
// 保护等待队列的锁
SpinLock m_gaurd;
// 持有锁的协程id
uint64_t m_fiberId = 0;
// 协程等待队列
std::queue<std::shared_ptr<Fiber>> m_waitQueue;
};
成员m_waitQueue
就是用户态维护的等待队列,维护等待这个锁的协程。
成员函数lock
的主要代码如下
void lock() {
...
// 第一次尝试获取锁
while (!tryLock()) {
// 由于进入等待队列和出队的代价比较大,所以再次尝试获取锁,
// 成功获取锁就返回
if(tryLock()){
...
return;
}
// 获取所在的协程
auto self = GetTHisFiber();
// 将自己加入协程等待队列
m_waitQueue.push(self);
// 让出协程
Yield;
}
...
}
我们尝试获取锁,如果获取失败就把自己放入等待队列并让出协程。
成员函数unlock
的主要代码如下
void unlock() {
...
auto Fiber = m_waitQueue.front();
...
// 释放协程锁
m_mutex.unlock();
...
// 将等待的协程重新加入调度
Schedule(fiber);
...
}
我们取出等待这个锁的协程,释放锁后将协程重新加入调度器。
通过一个很简单方式,我们在用户空间实现了互斥量。
使用样例
CoMutex mutex;
void a()