上篇文章libco协程切换原理详解中分析了协程的切换原理,并且也说到了建议用libgo代替libco,因为libgo的协程切换原理是从boost中分离出来的,其实现原理和libco基本上是一样的,不再分析,这里我们主要分析一下libgo多线程协程的管理和调度,因为这一部分libco是没有实现的。首先我们看一段多线程示例。
/************************************************
* libgo sample1
*************************************************/
#include "coroutine.h"
#include "win_exit.h"
#include <stdio.h>
#include <thread>
void foo()
{
printf("function pointer\n");
}
struct A {
void fA() { printf("std::bind\n"); }
void fB() { printf("std::function\n"); }
};
int main()
{
//----------------------------------
// 使用关键字go创建协程, go后面可以使用:
// 1.void(*)()函数指针, 比如:foo.
// 2.也可以使用无参数的lambda, std::bind对象, function对象,
// 3.以及一切可以无参调用的仿函数对象
// 注意不要忘记句尾的分号";".
go foo;
go []{
printf("lambda\n");
};
go std::bind(&A::fA, A());
std::function<void()> fn(std::bind(&A::fB, A()));
go fn;
// 也可以使用go_stack创建指定栈大小的协程
// 创建拥有10MB大栈的协程
go co_stack(10 * 1024 * 1024) []{
printf("large stack\n");
};
// 协程创建以后不会立即执行,而是暂存至可执行列表中,等待调度器调度。
// co_sched是默认的协程调度器,用户也可以使用自创建的协程调度器。
// 当仅使用一个线程进行协程调度时, 协程地执行会严格地遵循其创建顺序.
// 仅使用主线程调度协程.
// co_sched.Start();
// 以下代码可以使用等同于cpu核心数的线程调度协程.(包括主线程)
// co_sched.Start(0);
// 以下代码允许调度器自由扩展线程数,上限为1024.
// 当有线程被协程阻塞时, 调度器会启动一个新的线程, 以此保障
// 可用线程数总是等于Start的第一个参数(0表示cpu核心数).
// co_sched.Start(0, 1024);
// 如果不想让调度器卡住主线程, 可以使用以下方式:
std::thread t([]{ co_sched.Start(); });
t.detach();
co_sleep(100);
//----------------------------------
//----------------------------------
// 除了上述的使用默认的调度器外, 还可以自行创建额外的调度器,
// 协程只会在所属的调度器中被调度, 创建额外的调度器可以实现业务间的隔离.
// 创建一个调度器
co::Scheduler* sched = co::Scheduler::Create();
// 启动4个线程执行新创建的调度器
std::thread t2([sched]{ sched->Start(4); });
t2.detach();
// 在新创建的调度器上创建一个协程
go co_scheduler(sched) []{
printf("run in my scheduler.\n");
};
co_sleep(100);
return 0;
}
示例中可以看到类Scheduler是管理调度的基本单位,是一个代表一个管理调度线程,如果我们不自己创建会有一个默认的来负载管理和调度协程。关系图如下:
关系很多简单,一个Scheduler线程负责管理n个Processer线程,每个Processer线程中有一个task队列,保存着我们要执行的任务,也就是通过go关键字创建的协程,Scheduler负责收集n个Processe的负载值,空闲状态,打阻塞标记,如果某个Processer线程执行一个会导致线程阻塞的任务,那么阻塞后,Scheduler线程负责唤醒阻塞的线程并把执行栈切换到别的任务中执行充分利用cpu,如果某个Processe线程任务过多也会偷取任务放到别的Processe线程中达到负载均衡的目的。
下面看具体实现,先看Processer线程是如何执行任务队列的,首先.Process勒种有三个队列分别保存了几种任务类型队列
// 协程队列
typedef TSQueue<Task, true> TaskQueue; //多线程访问需要加锁
TaskQueue runnableQueue_; //可运行任务队列
TaskQueue waitQueue_; //阻塞任务队列
TSQueue<Task, false> gcQueue_; //准备释放的任务队列,单线程访问不需要加锁
TaskQueue newQueue_; //新添加的任务队列
.下面是最重要的Processer线程的执行函数Process()函数,如下
void Processer::Process()
{
GetCurrentProcesser() = this;
#if defined(LIBGO_SYS_Windows)
FiberScopedGuard sg;
#endif
while (!scheduler_->IsStop())
{
//取出一个协程(也就是一个任务task)放到runningTask_中
runnableQueue_.front(runningTask_);
//runnableQueue_中没有任务可以执行
if (!runningTask_) {
//AddNewTasks会把newQueue_队列移动到runnableQueue_中
if (AddNewTasks())
//再次差尝试取出一个协程(也就是一个任务task)放到runningTask_中
runnableQueue_.front(runningTask_);
//仍然没有任务可执行
if (!runningTask_) {
//阻塞等待任务到来,等待被唤醒(在改线程被唤醒之前,会阻塞在这里)
WaitCondition();
//运行到这里说明线程被唤醒后了,被唤醒后再次执行AddNewTasks把newQueue_队列移动到runnableQueue_中
AddNewTasks();
continue;
}
}
#if ENABLE_DEBUGGER
DebugPrint(dbg_scheduler, "Run [Proc(%d) QueueSize:%lu] --------------------------", id_, RunnableSize());
#endif
addNewQuota_ = 1;
//如果取到了可以执行的任务(注意这里是一个while死循环,执行到runningTask_结束,在循环中会修改runningTask_的值)
while (runningTask_ && !scheduler_->IsStop()) {
//修改任务状态为TaskState::runnable
runningTask_->state_ = TaskState::runnable;
//设置任务的执行Process为当前Process
runningTask_->proc_ = this;
#if ENABLE_DEBUGGER
DebugPrint(dbg_switch, "enter task(%s)", runningTask_->DebugInfo());
if (Listener::GetTaskListener())
Listener::GetTaskListener()->onSwapIn(runningTask_->id_);
#endif
//累加协程切换计数器
++switchCount_;
//切入执行任务协程,执行该任务
runningTask_->SwapIn();
#if ENABLE_DEBUGGER
DebugPrint(dbg_switch, "leave task(%s) state=%d", runningTask_->DebugInfo(), (int)runningTask_->state_);
#endif
//runningTask_协程切出后会执行到这里,也就是调用Process::StaticCoYield()进而调用runningTask_->SwapOut();
switch (runningTask_->state_) {
//如果当前任务还是TaskState::runnable状态,说明在任务执行完成前任务自己主动调用了runningTask_->SwapOut(),切出了,还没有执行结束
case TaskState::runnable:
{
//锁住runnableQueue_
std::unique_lock<TaskQueue::lock_t> lock(runnableQueue_.LockRef());
//取下一个任务
auto next = (Task*)runningTask_->next;
//如果有下一个任务
if (next) {
//把runningTask_替换为runningTask_(注意之前的runningTask_并没有从runnableQueue_中删掉,
//因为任务提前SwapOut了,还没有执行结束,等下次调度执行)
runningTask_ = next;
runningTask_->check_ = runnableQueue_.check_;
break;
}
//如果addNewQuota_ < 1或者没有新的任务则跳出循环
if (addNewQuota_ < 1 || newQueue_.emptyUnsafe()) {
runningTask_ = nullptr;
} else {
lock.unlock();
/*执行AddNewTasks把newQueue_队列移动到runnableQueue_中,同时-- addNewQuota_,这里最多执行一次,
*在协程中创建协程会放到newQueue_中,每轮调度只加有限次数新协程, 防止协程创建新协程产生死循环
*/
if (AddNewTasks()) {
//取出新的协程任务等待执行
runnableQueue_.next(runningTask_, runningTask_);
-- addNewQuota_;
} else {
//跳出循环
std::unique_lock<TaskQueue::lock_t> lock2(runnableQueue_.LockRef());
runningTask_ = nullptr;
}
}
}
break;
//如果当前任务阻塞,执行下一个任务
case TaskState::block:
{
std::unique_lock<TaskQueue::lock_t> lock(runnableQueue_.LockRef());
nextTask_ = nullptr;
runningTask_ = nextTask_;
}
break;
//任务完成
case TaskState::done:
default:
{
//从runnableQueue_取出下一个任务
runnableQueue_.next(runningTask_, nextTask_);
/*执行AddNewTasks把newQueue_队列移动到runnableQueue_中,同时-- addNewQuota_,这里最多执行一次,
*在协程中创建协程会放到newQueue_中,每轮调度只加有限次数新协程, 防止协程创建新协程产生死循环
*/
if (!nextTask_ && addNewQuota_ > 0) {
if (AddNewTasks()) {
runnableQueue_.next(runningTask_, nextTask_);
-- addNewQuota_;
}
}
DebugPrint(dbg_task, "task(%s) done.", runningTask_->DebugInfo());
//从runnableQueue_删除执行完成的任务
runnableQueue_.erase(runningTask_);
if (gcQueue_.size() > 16)
GC(); //释放任务资源(集中释放和立即释放有什么区别?减少内存碎片(¬‸¬)?)
gcQueue_.push(runningTask_); //把删除的任务放入gc队列中
if (runningTask_->eptr_) { //如果执行过程中有异常
std::exception_ptr ep = runningTask_->eptr_;
std::rethrow_exception(ep);
}
// runningTask_ = nextTask_;执行下一个任务
std::unique_lock<TaskQueue::lock_t> lock(runnableQueue_.LockRef());
runningTask_ = nextTask_;
nextTask_ = nullptr;
}
break;
}
}
}
}
最后我们看Scheduler的执行函数:DispatcherThread()函数
void Scheduler::DispatcherThread()
{
DebugPrint(dbg_scheduler, "---> Start DispatcherThread");
typedef std::size_t idx_t;
while (!stop_) {
//每1000微妙调度一次(可配置)
std::this_thread::sleep_for(std::chrono::microseconds(CoroutineOptions::getInstance().dispatcher_thread_cycle_us));
// 1.收集负载值, 收集阻塞状态, 打阻塞标记, 唤醒处于等待状态但是有任务的P
idx_t pcount = processers_.size();
std::size_t totalLoadaverage = 0;
typedef std::multimap<std::size_t, idx_t> ActiveMap;
ActiveMap actives;
std::map<idx_t, std::size_t> blockings;
int isActiveCount = 0;
for (std::size_t i = 0; i < pcount; i++) {
auto p = processers_[i];
//如果调度单位p已经阻塞
if (p->IsBlocking()) {
//记录当前可执行的任务数量:runnableQueue_.size() + newQueue_.size();
blockings[i] = p->RunnableSize();
if (p->active_) { //如果p->active_为true标记为false(p->active_只在DispatcherThread中修改和访问,线程安全)
p->active_ = false;
DebugPrint(dbg_scheduler, "Block processer(%d)", (int)i);
}
}
//累计当前活跃的调度器processers
if (p->active_)
isActiveCount++;
}
// 还可激活几个P
int activeQuota = isActiveCount < minThreadNumber_ ? (minThreadNumber_ - isActiveCount) : 0;
for (std::size_t i = 0; i < pcount; i++) {
auto p = processers_[i];
//计算负载
std::size_t loadaverage = p->RunnableSize();
//统计当前所有执行线程的负载
totalLoadaverage += loadaverage;
//如果该调度器是非活跃状态
if (!p->active_) {
//如果有可激活的调度器并且该调度器非阻塞
if (activeQuota > 0 && !p->IsBlocking()) {
//标记为活跃
p->active_ = true;
activeQuota--;
DebugPrint(dbg_scheduler, "Active processer(%d)", (int)i);
lastActive_ = i;
}
}
//如果该调度器是活跃状态,则插入活跃ActiveMap中,后面会把阻塞调度器的任务分到这些活跃的调度器中执行
if (p->active_) {
actives.insert(ActiveMap::value_type{loadaverage, i});
//给该调度器打标记,标记该调度器没有阻塞
p->Mark();
}
//如果有任务需要执行并且调度器阻塞等到中,则唤醒该调度器开始执行任务
if (loadaverage > 0 && p->IsWaiting()) {
p->NotifyCondition();
}
}
//如果可用的调度器是空的,切调度器数量没有达到最大,启动新的线程,创建新的调度器调度任务
if (actives.empty() && (int)pcount < maxThreadNumber_) {
// 全部阻塞, 并且还有协程待执行, 起新线程
NewProcessThread();
actives.insert(ActiveMap::value_type{0, pcount});
++pcount;
}
// 全部阻塞并且不能起新线程, 无需调度, 等待即可
if (actives.empty())
continue;
// 负载均衡1
// 阻塞线程的任务steal出来
{
SList<Task> tasks;
for (auto &kv : blockings) {
auto p = processers_[kv.first];
//把阻塞的调度器中任务Steal到tasks
tasks.append(p->Steal(0));
}
if (!tasks.empty()) {
//取出可执行任务最少的一组调度器(可能会有多个)
auto range = actives.equal_range(actives.begin()->first);
// 任务最少的几个线程平均分
std::size_t avg = tasks.size() / std::distance(range.first, range.second);
if (avg == 0)
avg = 1;
ActiveMap newActives;
//把可执行任务最少的调度器剩下的可执行调度器插入到newActives
for (auto it = range.second; it != actives.end(); ++it) {
newActives.insert(*it);
}
//把steal出来的任务平均分给可执行任务最少的一组调度器中调度
for (auto it = range.first; it != range.second; ++it) {
SList<Task> in = tasks.cut(avg);
if (in.empty())
break;
auto p = processers_[it->second];
p->AddTask(std::move(in));
//插入到newActives
newActives.insert(ActiveMap::value_type{p->RunnableSize(), it->second});
}
//如果还有剩余的任务,给最少的一组调度器的第一个调度器(上面已经均分过了,剩下的应该属平分后剩下的,浮点数除法向下取整造成的)
if (!tasks.empty())
processers_[range.first->second]->AddTask(std::move(tasks));
for (auto it = range.first; it != range.second; ++it) {
auto p = processers_[it->second];
newActives.insert(ActiveMap::value_type{p->RunnableSize(), it->second});
}
//交换newActives和actives(为啥要交换直接把newActives赋给actives不更好吗(¬‸¬)?)
newActives.swap(actives);
}
}
/* 如果还有在等待的线程, 从任务多的线程中拿一些给它
* 负载均衡2
* 任务多的线程的任务steal出来
* 如果有可执行任务数是0的调度器(下面主要做负载均衡,把任务多的调度器分分出去一些)
*/
if (actives.begin()->first == 0) {
auto range = actives.equal_range(actives.begin()->first);
std::size_t waitN = std::distance(range.first, range.second);
//没有可执行任务数是0的调度器
if (waitN == actives.size()) {
continue;
}
//取出任务最多的调度器
auto maxP = processers_[actives.rbegin()->second];
//计算要偷取的数量
std::size_t stealN = (std::min)(maxP->RunnableSize() / 2, waitN * 1024);
if (!stealN)
continue;
//偷取任务
auto tasks = maxP->Steal(stealN);
if (tasks.empty())
continue;
//平分
std::size_t avg = tasks.size() / waitN;
if (avg == 0)
avg = 1;
for (auto it = range.first; it != range.second; ++it) {
SList<Task> in = tasks.cut(avg);
if (in.empty())
break;
auto p = processers_[it->second];
//添加任务到调度器中
p->AddTask(std::move(in));
}
if (!tasks.empty())
processers_[range.first->second]->AddTask(std::move(tasks));
}
}
}
关于打阻塞标记简单说下,首先在Processer函数中有这样一句代码
//累加协程切换计数器
++switchCount_;
//切入执行任务协程,执行该任务
runningTask_->SwapIn();
可以回头看一看,这里Processer线程每次切入一个新的协程都会做一个计数++switchCount_,表示协程切换的次数。然后在DispatcherThread()中每隔dispatcher_thread_cycle_us检测一次如果线程是active状态会调用Mark函数打标机:
void Processer::Mark()
{
if (runningTask_ && markSwitch_ != switchCount_) {
markSwitch_ = switchCount_;
markTick_ = NowMicrosecond();
}
}
如果markSwitch_ != switchCount_就会记录mark的时间在markTick_ 中(注意这里的NowMicrosecond();是当前的系统稳定时间std::chrono::steady_clock base_clock_t,当前时间不会随着系统时间的调整而前进或者倒退而是由系统运行的周期数决定的),如果一个切入一个协程的时间过长没有切出切入新的协程就会判断为阻塞状态。
bool Processer::IsBlocking()
{
if (!markSwitch_ || markSwitch_ != switchCount_) return false;
return NowMicrosecond() > markTick_ + CoroutineOptions::getInstance().cycle_timeout_us;
}
最后看一下线程间相互偷取任务的实现,很简单:
SList<Task> Processer::Steal(std::size_t n)
{
if (n > 0) {
// steal some
newQueue_.AssertLink();
auto slist = newQueue_.pop_back(n);
newQueue_.AssertLink();
if (slist.size() >= n)
return slist;
std::unique_lock<TaskQueue::lock_t> lock(runnableQueue_.LockRef());
bool pushRunningTask = false, pushNextTask = false;
//先把runningTask_从可执行队列中删掉
if (runningTask_)
pushRunningTask = runnableQueue_.eraseWithoutLock(runningTask_, true) || slist.erase(runningTask_, newQueue_.check_);
//先把nextTask_从可执行队列中删掉
if (nextTask_)
pushNextTask = runnableQueue_.eraseWithoutLock(nextTask_, true) || slist.erase(nextTask_, newQueue_.check_);
//偷取任务
auto slist2 = runnableQueue_.pop_backWithoutLock(n - slist.size());
//把runningTask_还给runnableQueue_
if (pushRunningTask)
runnableQueue_.pushWithoutLock(runningTask_);
//把nextTask_还给runnableQueue_
if (pushNextTask)
runnableQueue_.pushWithoutLock(nextTask_);
lock.unlock();
slist2.append(std::move(slist));
if (!slist2.empty())
DebugPrint(dbg_scheduler, "Proc(%d).Stealed = %d", id_, (int)slist2.size());
return slist2;
} else {
// steal all
newQueue_.AssertLink();
auto slist = newQueue_.pop_all();
newQueue_.AssertLink();
std::unique_lock<TaskQueue::lock_t> lock(runnableQueue_.LockRef());
bool pushRunningTask = false, pushNextTask = false;
//先把runningTask_从可执行队列中删掉
if (runningTask_)
pushRunningTask = runnableQueue_.eraseWithoutLock(runningTask_, true) || slist.erase(runningTask_, newQueue_.check_);
//先把nextTask_从可执行队列中删掉
if (nextTask_)
pushNextTask = runnableQueue_.eraseWithoutLock(nextTask_, true) || slist.erase(nextTask_, newQueue_.check_);
//偷取任务
auto slist2 = runnableQueue_.pop_allWithoutLock();
//把runningTask_还给runnableQueue_
if (pushRunningTask)
runnableQueue_.pushWithoutLock(runningTask_);
//把nextTask_还给runnableQueue_
if (pushNextTask)
runnableQueue_.pushWithoutLock(nextTask_);
lock.unlock();
slist2.append(std::move(slist));
if (!slist2.empty())
DebugPrint(dbg_scheduler, "Proc(%d).Stealed all = %d", id_, (int)slist2.size());
return slist2;
}
}