很早之前在lua中實現(xiàn)過一版協(xié)程恰画,lua的棧是虛擬的宾茂,當(dāng)要切換協(xié)程時虛擬棧不需要退棧,只需要從C的棧(物理棧)退出拴还】缜纾恢復(fù)也簡單,直接在lua的虛擬棧壓入返回值片林,lua就可以繼續(xù)運行了端盆。這版協(xié)程比較簡單,雖然不支持lua的棧與C棧交替存在费封,但當(dāng)時也沒這種需求焕妙,運行的也算問題,沒什么特別的Bug弓摘。
最近在看Go焚鹊,開始以為Go的routine也是虛擬棧,很快發(fā)現(xiàn)不對韧献,Go是編譯語言末患,不是解釋語言,Go的棧是物理棧锤窑。當(dāng)時就想物理棧退棧了怎么再回到現(xiàn)場的璧针,搜了一下資料才發(fā)現(xiàn)自己秀逗了。物理棧不需要退棧渊啰,切換時保存好指令寄存器和棧指針寄存器探橱,恢復(fù)的時候再把3個寄存器一恢復(fù)就行了。反而比虛擬棧還要簡單些绘证,順著原理自己簡單的實現(xiàn)了一把隧膏,雖然過程有點曲折,效果還算滿意的嚷那。
首先是P私植,只包含了一個G的隊列
#include <queue>
class G;
class P
{
public:
void addG(G* g)
{
gs.push(g);
}
G* popG()
{
if (gs.size() == 0)
{
return nullptr;
}
G* g = gs.front();
gs.pop();
return g;
}
private:
std::queue<G*> gs;
};
G 用ID來模擬線程存儲數(shù)據(jù),esp_變量保存棧頂指針
//G.H
typedef void(__stdcall *routine)(int);
namespace runtime
{
void yield();
}
class M;
class G
{
public:
G(routine fun, int arg)
{
id = _id++;
//16MB的棧车酣,開始申請了4KB結(jié)果溢出了 各種Bug 以為棧指針操作出了問題
const int stackSize = 16 * 1024 * 1024;
//構(gòu)建堆棧
BYTE* mem = (BYTE*)malloc(stackSize + 1024); //多申請1KB
DWORD* p = (DWORD*)(mem + stackSize);
*p-- = (DWORD)mem;
*p-- = (DWORD)this;
*p-- = arg;
*p-- = (DWORD)fun;
*p-- = (DWORD)(mem + stackSize); //EBP
*p = 0; //標(biāo)識位 0表示routine還沒有運行棧曲稼,棧中都是數(shù)據(jù)
esp_ = (DWORD)p;
dead_ = false;
}
private:
G() //g0的構(gòu)造函數(shù)
{
id = _id++;
dead_ = false;
//g0不需要棧,用系統(tǒng)的棧
}
private:
void Attach(M* m)
{
m_ = m;
if (m != nullptr)
{
current_ = this;
}
else
{
current_ = nullptr;
}
}
public:
static G* GetCurrent()
{
return current_;
}
int getId()
{
return id;
}
private:
int id;
bool dead_;
M* m_;
DWORD esp_;
friend class M;
private:
static thread_local G* current_;
static int _id;
};
//G.cpp
thread_local G* G::current_;
int G::_id;
void runtime::yield()
{
M::GetCurrent()->yieldCurrentG();
}
最后是M湖员,G的切換贫悄,沒實現(xiàn)調(diào)度
//M.h
class M
{
public:
M(P* p);
~M();
public:
void switchG();
void yieldCurrentG();
void addG(G* g);
static M* GetCurrent()
{
return current_;
}
private:
void freeG(G* g, PVOID pStackMem);
private:
G* currentG_;
G* g0;
P* p_;
static thread_local M* current_;
};
//M.cpp
thread_local M* M::current_;
M::M(P* p)
{
p_ = p;
g0 = new G();
currentG_ = g0;
g0->Attach(this);
current_ = this;
}
M::~M()
{
delete g0;
current_ = nullptr;
}
void M::addG(G * g)
{
p_->addG(g);
}
__declspec(naked) void resumeG()
{
__asm
{
pop edi //return address
pop ecx //this M的指針
pop eax //參數(shù)
push edi
push ecx
mov esp, eax
sub esp, 8 //上上行 push了一個EDI,一個ECX
add eax, 36
mov ebp, eax //比pushad大點
mov edx, ecx
pop ecx //this
pop edi //return address
pop eax //flag
cmp eax, 0
jne RESUME
//新routine 初始化調(diào)用棧
pop ebp
pop eax
pop esi
push edx //保留this指針
push esi
call eax
pop ecx //this指針
call M::freeG //freeG不會返回了
RESUME:
pop eax
mov esp, eax
add eax, 36
mov ebp, eax //比pushad大點
pop eax
mov ebp, eax
push edi
ret
}
}
void M::freeG(G* g, PVOID pStackMem)
{
if (g == currentG_)
{
g->esp_ = (DWORD)pStackMem;
g->dead_ = true;
//切換到g0釋放內(nèi)存
DWORD esp_ = g0->esp_;
__asm
{
mov eax, esp_
push eax
mov ecx, this
push ecx
call resumeG
}
}
}
//g0堆棧
void M::switchG()
{
DWORD esp_ = 0;
while (true)
{
currentG_ = p_->popG();
if (currentG_ == nullptr)
{
//都運行完了
break;
}
__asm
{
mov eax, ebp
push eax
mov eax, esp
push eax
mov eax, 1
push eax
mov esp_, esp
}
g0->esp_ = esp_;
g0->Attach(nullptr);
currentG_->Attach(this);
esp_ = currentG_->esp_;
__asm
{
mov eax, esp_
push eax
mov eax, this
push eax
call resumeG
}
if (currentG_->dead_)
{
//此時在g0娘摔,但currentG不是g0
free((void*)currentG_->esp_);
delete currentG_;
currentG_ = nullptr;
}
}
}
void M::yieldCurrentG()
{
//保存現(xiàn)場
DWORD esp_ = 0;
G* g = currentG_;
__asm
{
mov eax, dword ptr[ebp - 4]
mov eax, ebp
push eax
mov eax, esp
push eax
mov eax, 1
push eax
mov esp_, esp
}
g->esp_ = esp_;
g->Attach(nullptr);
p_->addG(g);
//切換到g0
g0->Attach(this);
esp_ = g0->esp_;
__asm
{
mov eax, esp_
push eax
mov eax, this
push eax
call resumeG
}
//從其他routine回來了
currentG_ = g;
g->Attach(this);
}
所有的G都會從
resumeG
開始窄坦,resumeG
切換堆棧,進(jìn)入G的routine函數(shù)
執(zhí)行,由于沒實現(xiàn)調(diào)度鸭津,所以需要代碼中調(diào)用runtime::yield
來切換routine彤侍。runtime::yield
會調(diào)用M::yieldCurrentG
,M::yieldCurrentG
保存現(xiàn)場后切換到g0
逆趋,g0
選擇下一個routine盏阶,再通過resumeG
切換堆棧和指令寄存器,yieldCurrentG
得以繼續(xù)執(zhí)行闻书。routine函數(shù)
執(zhí)行完成后會回到最初的resumeG
名斟,resumeG
再調(diào)用M::freeG
來銷毀G,至此routine完結(jié)銷毀魄眉。
//測試代碼
void __stdcall routine1(int arg)
{
G* g = G::GetCurrent();
printf("fun=routine1, id=%d, arg=%d\n", g->getId(), arg);
runtime::yield();
printf("fun=routine1, id=%d, arg=%d\n", g->getId(), arg);
}
void __stdcall routine2(int arg)
{
G* g = G::GetCurrent();
printf("fun=routine2, id=%d, arg=%d\n", g->getId(), arg);
runtime::yield();
printf("fun=routine2, id=%d, arg=%d\n", g->getId(), arg);
}
int main()
{
P p;
M m(&p);
m.addG(new G(&routine1, 1));
m.addG(new G(&routine2, 1));
m.addG(new G(&routine1, 2));
m.addG(new G(&routine2, 2));
m.switchG();
return 0;
}
最關(guān)鍵就是M默認(rèn)的g0持有的是原生的系統(tǒng)棧砰盐,對其他routine的棧的操作都要在g0中進(jìn)行。避免內(nèi)存溢出坑律,堆棧破壞岩梳。