iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > GO >GoLang协程库libtask学习笔记
  • 863
分享到

GoLang协程库libtask学习笔记

GoLang协程库libtaskGoLanglibtask 2022-12-17 15:12:25 863人浏览 独家记忆
摘要

目录协程解决了什么问题简介对协程的抽象如何保存上下文信息协程的调度总结协程解决了什么问题 我们先从一次网络io请求过程中的read操作为例,请求数据会先拷贝到系统内核空间中,再从操作

协程解决了什么问题

我们先从一次网络io请求过程中的read操作为例,请求数据会先拷贝到系统内核空间中,再从操作系统的内核空间拷贝到应用程序的用户空间中。从内核空间将数据拷贝到用户空间过程中,会经历两个阶段:

  • 等待数据准备
  • 拷贝数据

因为有这两个阶段,所以就有了各种网络IO的模型:

同步编程:应用程序等待IO结果(比如等待打开一个大的文件,或者等待远端服务器的响应),阻塞当前线程

  • 优点:逻辑简单。
  • 缺点:效率太低,其他与IO无关的业务也要等待IO的响应。

异步多线程/进程:将IO操作频繁的逻辑、或者单纯的IO操作独立到一/多个线程中,业务线程与IO线程间靠通信/全局变量来共享数据。

  • 优点:充分利用CPU资源,防止阻塞资源。
  • 缺点:线程切换代价相对较高,异步逻辑代码复杂。

异步消息+回调函数:设计一个消息循环处理器,接收外部消息(包括系统通知和网络报文等),收到消息时调用注册的回调函数。

  • 优点:充分利用CPU资源,防止阻塞资源。
  • 缺点:代码逻辑复杂。

而协程,就是用同步的语义去解决异步问题,即业务逻辑看起来是同步的,但实际上并不阻塞当前线程(一般是靠事件循环处理来分发消息)。所以协程实际上是在单线程的环境下实现的应用程序级别的并发,就是把本来由操作系统控制的切换+保存状态在应用程序里面实现了。

由于协程在应用程序级别来处理任务,所以协程更像是一个函数,只是比普通的函数多了两个动作:yield()resume(),即让出和恢复。让出的时候我们需要将寄存器的协程上下文保存起来,恢复的时候再将上下文重新压入寄存器,继续执行。

简介

Libtask 是一个简单的协程库,它展示了最简单的一种协程实现方式。操作系统只能看见一个内核线程,无法感知到客户端协程的存在。

Libtask中的协程是协作式的,也就是说,使用的不是时间片轮转算法,调度器根据先来先服务的策略来执行就绪队列中的协程,只有当每个协程主动退出时调度器才会把CPU分配给下一个协程。

对协程的抽象

在libtask中,协程被抽象成一个Task结构体,结构体中的字段用于描述协程的相关信息:

// 一个Task可以看成是一个需要异步执行的任务,coroutine的抽象描述
struct Task
{
	char	name[256];
	char	state[256];
    // 前后指针
	Task	*next;
	Task	*prev;
	Task	*allnext;
	Task	*allprev;
    // 执行上下文
	Context	context;
    // 睡眠时间
	uvlong	alarmtime;
	uint	id;
    // 协程栈指针
	uchar	*stk;
    // 协程栈大小
	uint	stksize;
    // 协程是否退出了
	int	exiting;
    // 在在alltask的中的索引下标
	int	alltaskslot;
    // 是否是系统协程
	int	system;
    // 是否在就绪状态
	int	ready;
    // Task需要执行的函数
	void	(*startfn)(void*);
    // startfn的参数
	void	*startarg;
    // 自定义数据
	void	*udata;
};

创建协程

int taskcreate(void (*fn)(void*), void *arg, uint stack)
{
	int id;
	Task *t;
    // 分配task和stack的空间
	t = taskalloc(fn, arg, stack);
	// 协程的数量+1
	taskcount++;
	id = t->id;
	if(nalltask%64 == 0){
		alltask = realloc(alltask, (nalltask+64)*sizeof(alltask[0]));
		if(alltask == nil){
			fprint(2, "out of memory\n");
			abort();
		}
	}
    // 记录位置
	t->alltaskslot = nalltask;
    // 保存到alltask中
	alltask[nalltask++] = t;
    // 修改状态为就绪,可以被调度,并且加入到就绪队列
	taskready(t);
	return id;
}

我们可以使用taskcreate函数来创建协程,在taskcreate函数中,首先会调用taskalloc函数为Task和执行栈分配内存,然后初始化协程的上下文信息,在此之后,一个协程就被创建成功了。


static Task*
taskalloc(void (*fn)(void*), void *arg, uint stack)
{
	Task *t;
	sigset_t zero;
	uint x, y;
	ulong z;
	
	// 结构体本身的大小和栈大小
	// 协程栈大小是256*1024
	t = malloc(sizeof *t+stack);
	if(t == nil){
		fprint(2, "taskalloc malloc: %r\n");
		abort();
	}
	memset(t, 0, sizeof *t);
	// 栈的内存位置
	t->stk = (uchar*)(t+1);
	// 栈大小
	t->stksize = stack;
	// 协程id
	t->id = ++taskidgen;
	// 协程工作函数和参数
	t->startfn = fn;
	t->startarg = arg;
	
	memset(&t->context.uc, 0, sizeof t->context.uc);
	sigemptyset(&zero);
	// 初始化uc_sigmask字段为空,即不阻塞信号
	sigprocmask(SIG_BLOCK, &zero, &t->context.uc.uc_sigmask);
	
	// 初始化uc字段
	if(getcontext(&t->context.uc) < 0){
		fprint(2, "getcontext: %r\n");
		abort();
	}
	
	
	// 设置协程执行时的栈位置和大小
	t->context.uc.uc_stack.ss_sp = t->stk+8;
	t->context.uc.uc_stack.ss_size = t->stksize-64;
#if defined(__sun__) && !defined(__MAKECONTEXT_V2_SOURCE)		
#warning "doing sun thing"
	
	t->context.uc.uc_stack.ss_sp = 
		(char*)t->context.uc.uc_stack.ss_sp
		+t->context.uc.uc_stack.ss_size;
#endif
	
//print("make %p\n", t);
	z = (ulong)t;
	y = z;
	z >>= 16;	
	x = z>>16;
	// 保存信息到uc字段
	makecontext(&t->context.uc, (void(*)())taskstart, 2, y, x);
	return t;
}

创建好一个协程之后,taskcreate函数会调用taskready()函数把协程的状态修改为就绪态,并加入到就绪队列中。


void taskready(Task *t)
{
	t->ready = 1;
	addtask(&taskrunqueue, t);
}

如何保存上下文信息

我们可以发现,在调用taskalloc函数初始化协程的时候,我们还会对协程的上下文进行初始化,以下代码的流程是将当前CPU寄存器的上下文信息保存到当前Task的上下文中,同时将当前Task的栈位置和大小保存进上下文中,最后将协程的工作函数保存到上下文信息中。

	// 将上下文置为零值
	memset(&t->context.uc, 0, sizeof t->context.uc);
	// 将信号集zero清空
	sigemptyset(&zero);
	// 初始化uc_sigmask字段为空,即不阻塞信号
	sigprocmask(SIG_BLOCK, &zero, &t->context.uc.uc_sigmask);
	// 将当前上下文信息保存到t-context.uc结构体中
	if(getcontext(&t->context.uc) < 0){
		fprint(2, "getcontext: %r\n");
		abort();
	}
	// 设置协程执行时的栈位置和大小
	t->context.uc.uc_stack.ss_sp = t->stk+8;
	t->context.uc.uc_stack.ss_size = t->stksize-64;
	z = (ulong)t;
	y = z;
	z >>= 16;
	x = z>>16;
	// 设置协程的工作函数到上下文信息中
	makecontext(&t->context.uc, (void(*)())taskstart, 2, y, x);

ucontext族函数

其实对协程上下文的初始化以及保存是通过linux下的ucontext族函数来实现的。

ucontext_t结构体

我们发现Task结构体中有一个Context字段,这个字段其实就是对ucontext_t结构体的封装,ucontext_t结构体用于保存当前的上下文信息,它的结构是这样的:

typedef struct ucontext
  {
    unsigned long int uc_flags;
    struct ucontext *uc_link;//后序上下文
    __sigset_t uc_sigmask;// 信号屏蔽字掩码
    stack_t uc_stack;// 上下文所使用的栈
    // 保存的上下文的寄存器信息
    // 比如pc、sp、bp
    // pc程序计数器:记录下一条指令的地址
    // sp堆栈指针:指向函数调用栈栈顶的指针,所以新数据入栈将存入sp+1的地址
    // bp基址指针:指向函数调用栈的首地址
    mcontext_t uc_mcontext;
    long int uc_filler[5];
  } ucontext_t;
//其中mcontext_t 定义如下
typedef struct
  {
    gregset_t __ctx(gregs);//所装载寄存器
    fpregset_t __ctx(fpregs);//寄存器的类型
} mcontext_t;
//其中gregset_t 定义如下
typedef greg_t gregset_t[NGREG];//包括了所有的寄存器的信息

getcontext()函数

函数原型:

int getcontext(ucontext_t* ucp)

getcontext()函数的底层是通过汇编来实现的,其主要的功能是将当前运行到的寄存器信息保存到参数ucp中。

setcontext()函数

函数原型:

int setcontext(const ucontext_t *ucp)

setcontext()函数的作用是将ucontext_t结构体变量ucp中的上下文信息重新恢复到cpu中并执行。

makecontext()函数

函数原型:

void makecontext(ucontext_t *ucp, void (*func)(), int arGC, ...)

makecontext()函数的主要功能是设置协程的工作函数到上下文(ucontext_t)中,同时在用户设置的栈上保存一些信息,并且设置栈顶指针的值到上下文信息中。

argc是入口函数的参数个数,后面的...是具体的入口函数参数,该参数必须是整形值。

swapcontext()函数

函数原型:

int swapcontext(ucontext_t *oucp, ucontext_t *ucp)

该函数可以将当前cpu中的上下文信息保存到oucp结构体变量中,然后将ucp结构体的上下文信息恢复到cpu中。

这里可以理解为调用了两个函数,第一次是调用了getcontext(oucp)然后再调用setcontext(ucp)

协程的调度

在使用taskcreate创建协程的时候,这个函数内部会调用taskready函数修改新建协程的状态并加入就绪队列taskrunqueue中,taskrunqueue中的协程需要一个调度器来调度执行。

tasklib库中实现了一个协程调度中心的函数。调度中心会不断的从就绪队列中取出协程来执行,它的核心逻辑是这样的:

  • 从就绪队列中拿出一个协程t,并把t移出就绪队列。
  • 通过contextswitch函数将协程t的上下文信息切换到taskschedcontext中执行。
  • 将协程t切换回调度中心,如果t已经退出,修改数据结构,然后回收他的内存,然后继续调度其它的协程执行。这里的调度机制比较简单,是非抢占式的协作式调度,没有时间片的概念,一个协程的执行时间由自己决定,放弃执行的权力也是自己控制的,当协程不想执行了可以调用taskyield()函数让出cpu。
static void taskscheduler(void)
{
	int i;
	Task *t;
	taskdebug("scheduler enter");
	for(;;){
		// 如果没有就绪态协程了,就退出
		if(taskcount == 0)
			exit(taskexitval);
		// 从就绪队列中拿出一个协程
		t = taskrunqueue.head;
		if(t == nil){
			fprint(2, "no runnable tasks! %d tasks stalled\n", taskcount);
			exit(1);
		}
		// 从就绪队列中删除这个协程
		deltask(&taskrunqueue, t);
		 // 将协程状态改为非就绪态
		t->ready = 0;
		// 保存正在执行的协程
		taskrunning = t;
		// 切换次数+1
		tasknswitch++;
		taskdebug("run %d (%s)", t->id, t->name);
		// 切换到t执行,将当前cpu中的上下文信息保存到taskschedcontext中
        // 然后将t->context中的上下文信息恢复到cpu中执行
		contextswitch(&taskschedcontext, &t->context);
		// 执行结束
		taskrunning = nil;
		// 刚才执行的协程t退出了
		if(t->exiting){
			// 如果不是系统协程,协程个数减一
			if(!t->system)
				taskcount--;
			// 保存当前协程在alltask的索引
			i = t->alltaskslot;
			// 将最后一个协程切换到当前协程的位置,因为当前协程要退出了
			alltask[i] = alltask[--nalltask];
			// 更新被置换协程的索引
			alltask[i]->alltaskslot = i;
			// 释放堆内存
			free(t);
		}
	}
}

从上述调度器执行的代码中可以发现,我们使用contextswitch函数实现了协程间的上下文切换,这个函数的内部调用了swapcontext(&from->uc, &to->uc)函数,这个函数将当前cpu中的上下文信息保存到from结构体变量中,然后将to结构体的上下文信息恢复到cpu中执行。

执行结束之后,会重新将上下文切换回调度中心。

static void contextswitch(Context *from, Context *to)
{
	if(swapcontext(&from->uc, &to->uc) < 0){
		fprint(2, "swapcontext failed: %r\n");
		assert(0);
	}
}

其实我们还可以通过调用taskyield函数来控制协程在没有执行完就主动让出,那么当前正在执行的task会被 插入就绪队列的尾部,等待后续的调度,然后调度器会从就绪队列的头部重新取出一个task来执行。


int taskyield(void)
{
	int n;
	// 协程的让出次数
	n = tasknswitch;
	// 将当前主动让出的协程放进等待队列
	taskready(taskrunning);
	// 标记当前协程的状态为“让出”
	taskstate("yield");
	// 切换协程
	taskswitch();
	// 等于0说明当前只有自己一个协程,调度的时候taskswitch加1,所以这里要减1
	return tasknswitch - n - 1;
}

我们可以发现,切换流程的时候实际上是调用了taskswitch()函数,这个函数内部会调用contextswitch函数来切换上下文。


void taskswitch(void)
{
	needstack(0);
	// 将当前CPU中的上下文信息保存到taskrunning->context结构体中
	// 然后将调度中心上下文恢复到CPU中执行
	contextswitch(&taskrunning->context, &taskschedcontext);
}

总结

所以整个调度流程是这样的:

每一个协程对应一个Task结构体。然后调度中心不断地按照先进先出的方式去调度协程的执行就可以。因为没有抢占机制,所以调度中心是依赖协程本身去驱动的,协程需要主动让出cpu,把上下文切换回调度中心,调度中心才能进行下一轮的调度。

当然我们也可以调用taskyield函数主动让出CPU,他会将当前正在执行的task插入就绪队列的尾部,等待后续的调度,然后调度器会从就绪队列的头部重新取出一个task来执行。

到此这篇关于golang协程库libtask学习笔记的文章就介绍到这了,更多相关GoLang libtask内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

您可能感兴趣的文档:

--结束END--

本文标题: GoLang协程库libtask学习笔记

本文链接: https://www.lsjlt.com/news/175153.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • GoLang协程库libtask学习笔记
    目录协程解决了什么问题简介对协程的抽象如何保存上下文信息协程的调度总结协程解决了什么问题 我们先从一次网络IO请求过程中的read操作为例,请求数据会先拷贝到系统内核空间中,再从操作...
    99+
    2022-12-17
    GoLang协程库libtask GoLang libtask
  • [Python学习笔记] turtle库
    turtle库常用函数 引入turtle模块 import turtle turtle的绘图窗体 #setup()设置窗口大小及位置#setup()可省略turtle.setup(width,height,startx,st...
    99+
    2023-01-31
    学习笔记 Python turtle
  • JAVA编程学习笔记
    常用代码、特定函数、复杂概念、特定功能……在学习编程的过程中你会记录下哪些内容?快来分享你的笔记,一起切磋进步吧! 一、常用代码 在java编程中常用需要储备的就是工具类。包括封装的时间工具类。htt...
    99+
    2023-09-03
    java 学习 笔记
  • 学习笔记-TP5框架学习笔记\(路由\)
    TP5框架简单理解 (PS:只做粗略、关键知识的记录,TP程序的开始。详情请阅读官方手册) 1. 架构总览 TP程序的开始 PHP >=5.3.0, PHP7 ThinkPHP5.0应用基于MVC(模型-视图-控制器)的方...
    99+
    2023-10-25
    学习 php 开发语言
  • H3CNE学习笔记
      H3CNE五日“游” ——之第一天 废话少说 直接进入真题!!!!(哈哈 ) H3CNE   H3C认证初级网络工程师 第    一   节 路由器、交换机及其操作系统介绍 路由器 1、 路由器的作用 连接具有不同介质的链路 连接网络或...
    99+
    2023-01-31
    学习笔记 H3CNE
  • Python学习笔记之线程
    目录1.自定义进程2.进程与线程3.多线程4.Thread类方法5.多线程与多进程小Case6.Thread 的生命周期7.自定义线程8.线程共享数据与GIL(全局解释器锁)9.GI...
    99+
    2024-04-02
  • Python学习笔记
    Python介绍 Python是一种解释型、面向对象的语言。 官网:www.python.org Python环境 解释器:www.python.org/downloads 运行方式: 交互模式。在IDLE中运行。 脚本模式。文件的后缀...
    99+
    2023-01-30
    学习笔记 Python
  • python_os_sys学习笔记
    sys.argv 命令行参数List,第一个元素是程序本身路径 sys.exit(n) 退出程序,正常退出时exit(0) sys.version 获取Python解释程序的版本信息 sy...
    99+
    2023-01-31
    学习笔记 python_os_sys
  • tornado学习笔记
    一.UIMOTHODS: 1.在项目目录创建uimothods.py文件(名称可以任意)内容: def test2(self): return ('hello uimothods')2.tornado项目文件中导入并注册: #导入f...
    99+
    2023-01-30
    学习笔记 tornado
  • Python 学习笔记
    rs=Person.objects.all() all返回的是QuerySet对象,程序并没有真的在数据库中执行SQL语句查询数据,但支持迭代,使用for循环可以获取数据。 print rs.query 会打印出原生sql语句 rs=Pe...
    99+
    2023-01-31
    学习笔记 Python
  • 学习笔记(3)
    1.* 匹配零个或多个字符(通配符中)2.ls 的-d选项不仅仅可以显示指定目录的信息,还可以用来表示不递归子文件夹。  # ls -dl /etc 显示/etc目录的信息  # ls -d /etc 只显示/etc下面的文件夹3.显示/v...
    99+
    2023-01-31
    学习笔记
  • GNS3学习笔记
    最近在自学CCNA,为了搭建路由模拟器先后下载了Boson Network 、DynamipsGUI用的不是很顺手,后来听朋友推荐GNS3很好用,他们报的CCNP培训班老师用的就是GNS,平时的模拟实验都是用这个完成的,由于我本机已有下好的...
    99+
    2023-01-31
    学习笔记
  • python3学习笔记
    好久不用python,努力捡起来ing python3语法 字符串 repr()把其他类型变量转换为字符串 ord()把单个字符转换为相应的ascii码 int()把其他进制的“字符串”转换为十进制 int(str,n...
    99+
    2023-01-31
    学习笔记
  • Kafka 学习笔记
    😀😀😀创作不易,各位看官点赞收藏. 文章目录 Kafka 学习笔记1、消息队列 MQ2、Kafka 下载安装2.1、Zookeeper 方式启动2.2、KRaft 协议启动2.3...
    99+
    2023-08-30
    kafka 学习 笔记
  • MySQL 学习笔记
    😀😀😀创作不易,各位看官点赞收藏. 文章目录 MySQL 学习笔记1、`DQL` 查询语句1.1、基本查询1.2、函数查询1.2.1、单行函数1...
    99+
    2023-10-01
    mysql 学习 笔记
  • python3 学习笔记
    本人很少写 python 代码, 一般都是用 go 的, 去年时用 python 写过一些收集系统信息的工具, 当时是边看手册边写的. 如今又要用 python 来写一个生成 xlsx 的工具, 就又需要查看手册了, 至于为什么不用 g...
    99+
    2023-01-31
    学习笔记
  • 学习笔记3
    一文件查找和压缩1文件查找locate 搜索依赖于数据库,非实时搜索,搜索新建文件需手动更新,适于搜索稳定不频繁修改文件 find 实时搜索,精确搜索,默认当前目录递归搜索 find用法 -maxdepth...
    99+
    2023-01-31
    学习笔记
  • Java多线程学习笔记
    目录多任务、多线程程序、进程、线程学着看jdk文档线程的创建1.继承Thread类2.实现Runable接口理解并发的场景龟兔赛跑场景实现callable接口理解函数式接口理解线程的...
    99+
    2024-04-02
  • python学习笔记(三)—数据库篇
    一、数据库编程 数据库编程是指在应用程序中使用数据库管理系统(DBMS)进行数据存储、检索和处理的过程。数据库提供了一种结构化的方式来组织和存储数据,使得数据的管理更加高效和可靠。 1.1 关系数据库...
    99+
    2023-09-18
    python 学习 笔记
  • Python Paste 学习笔记
    一、写在前面 这篇文章主要介绍了Python的Paste库的使用,学习过程中主要参考官网文档以及自己的理解,整理成笔记以便后续自己查阅。 如果转载,请保留作者信息。 邮箱地址:jpzhang.ht@gmail.com ...
    99+
    2023-01-31
    学习笔记 Python Paste
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作