libgo原始碼分析之多執行緒協程管理和排程
上篇文章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;
}
}
相關文章
- Java排程執行緒池ScheduledThreadPoolExecutor原始碼分析Java執行緒thread原始碼
- RxJava原始碼解析(二)—執行緒排程器SchedulerRxJava原始碼執行緒
- Java併發和多執行緒3:執行緒排程和有條件取消排程Java執行緒
- swoole 協程原始碼解讀 (協程的排程)原始碼
- Golang原始碼學習:排程邏輯(三)工作執行緒的執行流程與排程迴圈Golang原始碼執行緒
- spark streaming原始碼分析3 排程及執行Spark原始碼
- 程序 執行緒 協程執行緒
- Java執行緒的排程Java執行緒
- 程式執行緒排程方式執行緒
- 程式、執行緒和協程的概念執行緒
- RxJava 和 RxAndroid 五(執行緒排程)RxJavaAndroid執行緒
- 多執行緒------執行緒與程式/執行緒排程/建立執行緒執行緒
- 程序中的執行緒排程執行緒
- 執行緒、程式與協程執行緒
- 協程、執行緒與程式執行緒
- 什麼是程式、執行緒和協程?執行緒
- Python之執行緒、程式和協程Python執行緒
- 程式執行緒協程關係執行緒
- Python執行緒、程式和協程詳解Python執行緒
- 原始碼分析OKHttp的執行過程原始碼HTTP
- knockout原始碼分析之執行過程原始碼
- 多執行緒-執行緒排程及獲取和設定執行緒優先順序執行緒
- quartz執行緒管理的原始碼分析quartz執行緒原始碼
- Nachos實驗實現執行緒id、限制執行緒數和更改排程演算法(按優先順序排程)執行緒演算法
- 多執行緒脫離狀態 + 排程執行緒
- OpenMP 中的執行緒任務排程執行緒
- 執行緒 、程式、協程的基本使用執行緒
- 程序、執行緒、協程的區別執行緒
- Informix 執行緒sleep 分析過程ORM執行緒
- golang 原始碼分析之scheduler排程器Golang原始碼
- 【協程原理】 - 協程不過是使用者態的執行緒執行緒
- 目前對程式、執行緒、協程的理解執行緒
- Python程式、執行緒、協程詳解Python執行緒
- 程式,核心執行緒,使用者執行緒,協程,纖程......作業系統世界觀執行緒作業系統
- OkHttp 知識梳理(2) OkHttp 原始碼解析之非同步請求 & 執行緒排程HTTP原始碼非同步執行緒
- Spring 非同步執行緒池、排程任務執行緒池配置Spring非同步執行緒
- laravel 應用層執行過程原始碼分析Laravel原始碼
- Python——程式、執行緒、協程、多程式、多執行緒(個人向)Python執行緒