對於作業系統而言,程序是最小的資源管理單元,執行緒是最小的執行單元。對於一個執行緒,在使用中,效能可能會受到以下因素的影響:
所以引入協程——更加輕量級的執行緒。
就像程序和執行緒的關係一樣,一個程序可以擁有多個執行緒,一個執行緒可以擁有多個協程。
協程是在使用者態執行的,所以不會像執行緒切換那樣消耗資源,使效能得到提升。可以說,執行緒是一個特殊的函數,這個函數可以在某個地方掛起,並且可以在掛起處繼續執行。一個執行緒內可以由多個這樣的特殊的函數在執行,但是一個執行緒的多個協程的執行是序列的。如果是多核CPU,多個程序或一個程序內的多個執行緒是可以並行執行的,但是一個執行緒內協程卻絕對是序列的。畢竟協程仍然是一個函數,一個執行緒內可以執行多個函數,但是每個函數都是序列執行的。當一個協程執行時,其他的協程必須被掛起。
C語言上下文切換的庫ucontext:
typedef struct ucontex {
stack_t uc_stack; // 當前上下文要用到的棧
mcontext_t uc_mctx; // 跟硬體相關的資訊
struct ucontext *uc_link; //儲存當前context結束後繼續執行的context記錄
sigset_t uc_sigmask; // 存的上下文的執行期間要遮蔽的訊號集合
}ucontext_t;
// stack_t結構體
typedef struct {
void *ss_sp; // 棧的起始位置
int ss_size; // 棧大小
int ss_flags; // 標誌
}stack_t;
寫個程式感受一波 getcontext() 和 setcontext()
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <ucontext.h>
int main() {
int i;
ucontext_t ctx;
getcontext(&ctx);
printf("i = %d\n",i++);
sleep(1);
setcontext(&ctx);
}
先解釋一波:首先 getcontext() 獲取上下文,然後往下邊執行,執行到 setcontext() 就會去執行getcontext() 獲取到的上下文,也就是說就會回到 getcontext() 所在位置繼續往下執行,從而形成一個迴圈,迴圈的列印 i 的值,結果如下:
再來個程式感受一波 makecontext() :
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <ucontext.h>
void fun() {
printf("fun()\n");
}
int main() {
int i = 1;
char *stack = (char*)malloc(sizeof(char) * 8192);
ucontext_t ctx_main;
ucontext_t ctx_fun;
getcontext(&ctx_main);
getcontext(&ctx_fun);
printf("i = %d\n",i++);
sleep(1);
ctx_fun.uc_stack.ss_sp = stack;
ctx_fun.uc_stack.ss_size = 8192;
ctx_fun.uc_stack.ss_flags = 0;
ctx_fun.uc_link = &ctx_main;
makecontext(&ctx_fun, fun, 0);
setcontext(&ctx_fun);
printf("main exit\n");
return 0;
}
解釋一波:main 函數首先 getcontext(),獲取 main 函數的上下文 ctx_main,ctx_fun 在下邊使用了 makecontext(),使得 ctx_fun 的上下文變成了 fun()函數,所以程式會先列印一個 i = X,接著去 fun 函數列印 fun() 因為 ctx_fun 的連線的 ctx_main 所以又陷入一個迴圈,列印完 i 的值,列印 fun()
來個程式碼感受 swapcontext();
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <ucontext.h>
ucontext_t ctx_f1;
ucontext_t ctx_f2;
ucontext_t ctx_main;
void fun1() {
printf("fun1() start\n");
swapcontext(&ctx_f1, &ctx_f2);
printf("fun1() end\n");
}
void fun2() {
printf("fun2() start\n");
swapcontext(&ctx_f2, &ctx_f1);
printf("fun2() end\n");
}
int main() {
char stack1[1024 * 8];
char stack2[1024 * 8];
getcontext(&ctx_main);
getcontext(&ctx_f1);
getcontext(&ctx_f2);
ctx_f1.uc_stack.ss_sp = stack1;
ctx_f1.uc_stack.ss_size = 8192;
ctx_f1.uc_stack.ss_flags = 0;
ctx_f1.uc_link = &ctx_f2;
makecontext(&ctx_f1, fun1, 0);
ctx_f2.uc_stack.ss_sp = stack2;
ctx_f2.uc_stack.ss_size = 8192;
ctx_f2.uc_stack.ss_flags = 0;
ctx_f2.uc_link = &ctx_main;
makecontext(&ctx_f2, fun2, 0);
swapcontext(&ctx_main, &ctx_f1);
printf("main exit\n");
}
解釋:
首先初始化 ctx_f1 ,它的後繼是 ctx_f2,接著 makecontext() 構造上下文,ctx_f1 的上下文是 fun1() 函數;
初始化 ctx_f2,它的後繼是 ctx_main ,接著 makecontext() 構造上下文, ctx_f2 的上下文是 fun2() 函數。
執行流程:首先主函數遇到 swapcontext(&ctx_main, &ctx_f1),切換到 fun1() 函數,進入 fun1() 函數遇到 swapcontext(&ctx_f1, &ctx_f2) ,切換到 fun2() 函數,進入fun2() 函數遇到 swapcontext(&ctx_f2, &ctx_f1) ,返回 fun1()函數,fun1() 函數執行完,去執行 fun1() 函數的後繼 fun2() ,fun2() 函數執行完,去執行 fun2() 函數的後繼主函數,效果如下:
----------------------------------------------------分割線----------------------------------------------------
所以現在要實現協程,協程的結構體中應該有:回撥函數、協程上下文、協程棧、協程狀態。還得有一個協程排程器來控制所有的協程,具體程式碼如下:
enum State {
DEAD,
READY,
RUNNING,
SUSPEND
};
struct schedule;
// 協程結構體
typedef struct {
// 回撥函數
void *(*call_back)(struct schedule *s, void* args);
// 回撥函數引數
void *args;
// 協程上下文
ucontext_t ctx;
// 協程棧
char stack[STACKSZ];
// 協程狀態
enum State state;
}coroutine_t;
// 協程排程器
typedef struct schedule {
// 所有協程
coroutine_t **coroutines;
// 當前正在執行的協程,如果沒有正在執行的協程為 -1
int current_id;
// 最大下標
int max_id;
// 主流程上下文
ucontext_t ctx_main;
}schedule_t;
協程還得有以下功能的函數,具體的實現,參照最開始的連結
// 建立協程排程器
schedule_t *schedule_creat();
// 協程執行函數
static void* main_fun(schedule_t *s);
// 建立協程,返回當前協程在排程器的下標
int coroutine_creat(schedule_t *s, void *(*call_back)(schedule_t *,void *args),void *args);
// 讓出 CPU
void coroutine_yield(schedule_t *s);
// 恢復 CPU
void coroutine_resume(schedule_t *s, int id);
// 刪除協程
static void delete_coroutine(schedule_t *s, int id);
// 釋放排程器
void schedule_destroy(schedule_t *s);
// 判斷所有協程都執行完了
int schedule_finish(schedule_t *s);
// 啟動協程
void coroutine_running(schedule_t *s, int id);
有了以上函數的功能,使用協程實現網路伺服器的具體思路如下:
做到一請求一協程,而不是一請求一執行緒。因為協程排程器裡邊是用陣列儲存的所有的協程,所以可以用一個迴圈一直去遍歷這個陣列,當哪個協程需要執行的時候,就執行,如果讓出CPU了,就回圈往後遍歷看下一個。在同步的情況下實現了非同步。用一個 current_id 來判斷當前協程是否執行(如果不執行 current_id 置為 -1)。