goroutine原理(C语言表示)

协程:

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()函数进行切换。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章