协程:
1、能够在单一的系统线程中模拟多个任务的并发执行。
2、在一个特定的时间,只有一个任务在运行,即并非真正地并行。
3、被动的任务调度方式,即任务没有主动抢占时间片的说法。当一个任务正在执行时,外 部没有办法中止它。要进行任务切换,只能通过由该任务自身调用yield()来主动出让 CPU使用权。
4、每个协程都有自己的堆栈和局部变量。
每个协程都包含3种运行状态:挂起、运行和停止。停止通常表示该协程已经执行完成(包 括遇到问题后明确退出执行的情况),挂起则表示该协程尚未执行完成,但出让了时间片,以后 有机会时会由调度器继续执行。
示例
package main
import (
"flag"
"fmt"
"os"
"strconv"
)
var goal int
func primetask(c chan int) {
for {
i := <-c
fmt.Printf("%d\n",i)
}
}
func main() {
c := make(chan int)
go primetask(c)
for i := 0;i < 100; i++ {
c <- i
}
}
对应C语言版本
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <task.h>
//goroutine函数
void primetask(void *arg) {
Channel *c = (Channel*)arg;
for(;;){
//从channel获取数据
int i = chanrecvul(c);
printf("%d\n",i)
}
}
void taskmain(int argc, char **argv) {
int i;
Channel *c;
//创建channel
c = chancreate(sizeof(unsigned long), buffer);
//创建goroutine并调度
taskcreate(primetask, c, 8192);
for(i=0;i < 100;++i)
//往channel中发送数据
chansendul(c, i);
}
原理剖析:
//结构体Task表示每一个goroutine
struct Task {
char name[256]; //名字
char state[256]; //状态
Task *next; //上一个任务
Task *prev; //下一个任务
Task *allnext;
Task *allprev;
Context context; //上下文
uvlong alarmtime;
uint id; //id
uchar *stk; //栈指针
uint stksize; //栈大小
int exiting; //是否退出
int alltaskslot; //在任务队列中的位置
int system;
int ready; //是否处于就绪状态
void (*startfn)(void*); //回调函数
void *startarg; //函数参数
void *udata;
};
static int taskidgen;
//为Task分配内存并初始化
static Task* taskalloc(void (*fn)(void*), void *arg, uint stack) {
Tat;
sigset_t zero;
uint x, y;
ulong z;
/* 一起分配任务和栈需要的内存*/
t = malloc(sizeof *t+stack);
if(t == NULL){
fprint(2, "taskalloc malloc: %r\n");
abort();
}
memset(t, 0, sizeof *t);
t->stk = (uchar*)(t+1);
t->stksize = stack;
t->id = ++taskidgen;
t->startfn = fn;
t->startarg = arg;
/* 初始化 */
memset(&t->context.uc, 0, sizeof t->context.uc);
sigemptyset(&zero);
sigprocmask(SIG_BLOCK, &zero, &t->context.uc.uc_sigmask);
/* 必须使用当前的上下文初始化*/
if(getcontext(&t->context.uc) < 0){
fprint(2, "getcontext: %r\n");
abort();
}
/* 调用makecontext来完成真正的工作 */
/* 头尾都留点空间。*/
t->context.uc.uc_stack.ss_sp = t->stk+8;
t->context.uc.uc_stack.ss_size = t->stksize-64;
print("make %p\n", t);
//z用于表示task的64位地址
//y保存低32位,x保存高32位
z = (ulong)t;
y = z;
z >>= 16;
x = z>>16;
//为每个task的上下文绑定一个函数
makecontext(&t->context.uc, (void(*)())taskstart, 2, y, x);
return t;
}
//每个上下文的回调函数
static void taskstart(uint y, uint x) {
//将x,y还原为task的地址
Task *t;
ulong z;
z = x<<16;
z <<= 16;
z |= y;
t = (Task*)z;
//执行task的回调函数
//print("taskstart %p\n", t);
t->startfn(t->startarg);
//print("taskexits %p\n", t);
taskexit(0);
//print("not reacehd\n");
}
//创建一个goroutine并加入到调度队列中
int taskcreate(void (*fn)(void*), void *arg, uint stack) {
int id;
Task *t;
t = taskalloc(fn, arg, stack);
taskcount++;
id = t->id;
//空间不足,重新分配
if(nalltask%64 == 0){
alltask = realloc(alltask, (nalltask+64)*sizeof(alltask[0]));
if(alltask == NULL){
fprint(2, "out of memory\n");
abort();
}
}
//加入到调度队列中
t->alltaskslot = nalltask;
alltask[nalltask++] = t;
taskready(t);
return id;
}
//调度程序
static void taskscheduler(void) {
int i;
Task *t;
taskdebug("scheduler enter");
for(;;){
if(taskcount == 0)
exit(taskexitval);
t = taskrunqueue.head;
if(t == NULL){
fprint(2, "no runnable tasks! %d tasks stalled\n", taskcount);
exit(1);
}
deltask(&taskrunqueue, t);
t->ready = 0;
taskrunning = t;
tasknswitch++;
taskdebug("run %d (%s)", t->id, t->name);
//关键就是这个函数,将调度器的上下文切换到goroutine的上下文,goroutine执行完后回到调度函数的上下文
contextswitch(&taskschedcontext, &t->context);
//print("back in scheduler\n");
taskrunning = NULL;
if(t->exiting){
if(!t->system)
taskcount--;
i = t->alltaskslot;
alltask[i] = alltask[--nalltask];
alltask[i]->alltaskslot = i;
free(t);
}
}
}
/*
goroutine让出cpu的情况有3种
1.自己执行完毕
2.主动让出(yield)
3.io阻塞
*/
//让出函数
int taskyield(void) {
//主要是将自己状态变为ready并放回到调度队列中,并将当前的上下文切换回调度器的上下文
int n;
n = tasknswitch;
taskready(taskrunning);
taskstate("yield");
taskswitch();
return tasknswitch - n - 1;
}
void taskswitch(void) {
needstack(0);
contextswitch(&taskrunning->context, &taskschedcontext);//切换回调度器的上下文
}
void taskready(Task *t) {
t->ready = 1;
addtask(&taskrunqueue, t);
}
//Channel的数据结构表示
struct Alt {
Channel *c;
void *v;
unsigned int op;
Task *task;
Alt *xalt;
};
struct Altarray {
Alt **a;
unsigned int n;
unsigned int m;
};
struct Channel {
unsigned int bufsize;
unsigned int elemsize;
unsigned char *buf;
unsigned int nbuf;
unsigned int off;
Altarray asend;
Altarray arecv;
char *name;
};
//Channel主要是维护了一个数据缓冲区,一个接收队列和发送队列
Channel* chancreate(int elemsize, int bufsize) {
Channel *c;
c = malloc(sizeof *c+bufsize*elemsize);
if(c == NULL){
fprint(2, "chancreate malloc: %r");
exit(1);
}
memset(c, 0, sizeof *c);
c->elemsize = elemsize;
c->bufsize = bufsize;
c->nbuf = 0;
c->buf = (uchar*)(c+1);
return c;
}
总结:
1.每个goroutine可以用一个Task数据结构表示;
2.每创建一个goroutine相当于创建一个Task数据结构并加入到调度队列中;
3.调度器对于每个Task的调度,主要是基于contextswitch()函数,将当前上下文切换到goroutine的上下文,goroutine执行完之后回到调度器的上下文,继续执行task的调度;
4.goroutine交还CPU的控制权(即将上下文切换回调度器)的方式有:执行结束、主动让出、io阻塞;
5.协程的执行是单线程的,可以将这个单线程看作是操作系统,每个协程看作是操作系统的一个进程。进程之间通过操作系统的schuel()调度函数进行切换,goroutine中通过contextswitch()函数进行切换。