协程详解以及网络IO的协程框架

协程详解以及网络IO的协程框架

简介:本文由异步编程引出协程,进而介绍了三种实现协程的方法。最后基于汇编方式仿造现有网络协程框架NtyCo仿写常用API。

引言

​ 最简单的串行同步编程中,所有的代码顺序执行,当前面的代码还未完成后续的代码只能等待;而并行异步编程,将操作交由其他线程通过回调函数处理后续代码无需持续等待,大大提高了程序的并行处理能力。例如有程序调用DNS解析多个域名。串行编程会依次将域名传给POSIX接口等待返回数据后再处理下一个;而并行编程不关心当前操作是否返回数据,直接将解析域名操作丢给回调函数,这样就能同时处理多个解析操作做到了并发编程。但并发编程依赖于回调函数,在大型的开发环境下往往会陷入回调地狱大大降低了代码的可读性。在这种情况下协程应运而生,它做到了同步编程的简单又具有异步编程的并发性。针对于不同场景本文介绍了不同的协程实现方法,并通过现有的NtyCo框架仿写了基本API。

原理及架构

一、协程的原理

​ 假设有如下场景:作为客户端通过dns服务器请求一系列域名的ip地址,分别使用串行和并行发起请求:

	//串行
	int count = sizeof(domain) / sizeof(domain[0]);
	int i = 0;

	for (i = 0;i < count;i ++) {
		dns_client_commit(domain[i]);
	}

​ 可以看出在串行中仅仅通过循环调用提交函数,且提交函数为单线程,即只有当前请求收到回应并处理完成后才会进入下一个循环。

	//并行
	struct async_context *ctx = dns_async_client_init();
	if (ctx == NULL) return -2;

	int count = sizeof(domain) / sizeof(domain[0]);
	int i = 0;

	for (i = 0;i < count;i ++) {
		dns_async_client_commit(ctx, domain[i], dns_async_client_result_callback);
		//sleep(2);
	}

​ 在并行中,使用poll和pthread线程接管请求事件,将请求dns服务器交给poll接管。实现并发请求,此时主线程代码会持续运行,若有返回即会打印在屏幕上。
​ 从最终运行结果也能看出来,串行会依次将结果打印出来,而并行因为是并发执行,会无序地将返回数据呈现。
​ 注意到这一行代码:

	dns_async_client_commit(ctx, domain[i], dns_async_client_result_callback);

​ 向dns服务器发起请求的操作通过回调函数dns_async_client_result_callback()实现,在这个简单的例子里面使用一层的回调函数就可以解决问题。但在实际开发中,往往会出现回调嵌套回调,大大降低了代码的可读性。所以有没有一种同时具备串行的可读性,且有并行的性能呢?

​ 所以协程应运而生,它就是解决这种情况下的工具。其本质就是在用户态进行切换的轻量级线程,通过串行的编码方式实现了异步的效果。在操作系统中,cpu作为资源被线程轮番抢占,程序在cpu上面运行随时都会出现中断;协程通过yield挂起主动将cpu让给其他协程,等待就绪后resume恢复。就精确地体现了它的工作原理:程序员将需要等待的程序的执行权主动让出给其他欲执行程序,从而做到无需回调也能并发执行的效果。

二、三种协程

  • setjmp/longjmp

​ 最原始的协程方案,便于理解协程的切换原理。该方案中setjmp()用于保存并挂起当前环境,longjmp()恢复并运行程序。如下是一个简单的协程跳转示意:

	jmp_buf env;

	void func(int arg) {
		printf("func: %d\n", arg);
		longjmp(env, ++arg);
	}

	int main() {
		int ret = setjmp(env); 
		if (ret == 0) {
			func(ret);
		} else if (ret == 1) {
			func(ret);
		} else if (ret == 2) {
			func(ret);
		} else if (ret == 3) {
			func(ret);
		}
		return 0;
	}

​ 当主函数第一次调用setjmp(),程序保存当前环境到env中并返回0,此时ret=0。进入func()中,longjmp(env,1)调转回setjmp()处并使其返回1,ret=1。以此类推。最后结果如下:

	func: 0
	func: 1
	func: 2
	func: 3

​ 这种方案的优点就是简单,同样也是缺点。实际生产环境严禁使用这种方案,因为func()函数在栈中的各种值并未被正确清理,多次跳转后会出现错误导致程序崩溃。

  • ucontext

​ ucontext方案集中在上下文控制块上:

	typedef struct ucontext {
    struct ucontext *uc_link;      // 当前上下文结束后跳转到的上下文
    sigset_t         uc_sigmask;   // 上下文阻塞的信号掩码
    stack_t          uc_stack;     // 上下文使用的栈信息
    mcontext_t       uc_mcontext;  // 机器相关的寄存器状态(CPU上下文)
} ucontext_t;

​ 一般情况下需要指定上下文中用于存放信息的栈。

​ 可以简单地理解为每一个协程对应一个ucontext_t类型的上下文控制块。类似地,ucontext也有用于跳转与恢复执行的函数:

	getcontext(&ctx)	//保存当前上下文到ctx
	setcontext(&ctx)	//立即跳转到指定上下文(不返回)
	makecontext(&ctx, func, argc, ...)	//绑定上下文到函数,需先设置uc_stack和uc_link
	swapcontext(&old, &new)	//保存当前上下文到old,并跳转到new

​ 假设有三个函数需要同步运行,为了实现协程操作需要初始化包括主函数在内的四个上下文控制块:

	ucontext_t ctx[3];
	ucontext_t main_ctx;

​ 在主函数中初始化控制块的所有信息:

	char stack1[2048] = {0};

	getcontext(&ctx[0]);
	ctx[0].uc_stack.ss_sp = stack1;
	ctx[0].uc_stack.ss_size = sizeof(stack1);
	ctx[0].uc_link = &main_ctx;
	makecontext(&ctx[0], func1, 0);

​ 按照一定规则切换协程:

	while (count <= 30) { // scheduler
		swapcontext(&main_ctx, &ctx[count%3]);
	}

​ 在每个协程中,执行完程序切换回主函数:

	void func1(void) {
		while (count ++ < 30) {
			printf("1\n");
			//swapcontext(&ctx[0], &ctx[1]);
			swapcontext(&ctx[0], &main_ctx);
			printf("4\n");
		}
	}

​ 在这里我们可以留意到,协程的主动切换都是在主函数中执行,换句话说要想协程发挥作用就必须要有一个主动切换协程的模块。据此就引出了scheduler的概念:作为协程系统的大脑,负责协调多个协程执行顺序、分配资源等功能。

graph TD A[Scheduler] --> B[任务队列] A --> C[就绪队列] A --> D[阻塞队列] A --> E[定时器队列] B -->|新建协程| F[协程控制块] C -->|可运行| F D -->|I/O阻塞| F E -->|超时唤醒| F

​ 介绍完了简单的协程切换,那么协程在IO场景下是如何发挥作用的呢?例如在简单的文件读写场景下,系统提供给我们的API:read/write并不具有协程功能,此时需要通过Hook机制将系统API拦截,在原始API函数之上添加自定义功能。

​ 定义两个API的函数指针,并将其初始化为空,便于后续操作:

	typedef ssize_t (*read_t)(int fd, void *buf, size_t count);
	read_t read_f = NULL;

	typedef ssize_t (*write_t)(int fd, const void *buf, size_t count);
	write_t write_f = NULL;

​ 被替换掉的原始API的操作:

	ssize_t read(int fd,void *buf,size_t count){
    	ssize_t ret = read_f(fd,buf,count);
    	printf("read: %s\n",(char *)buf);
    	return ret;
	}
	
	ssize_t write(int fd,const void *buf,size_t count){
    	printf("write: %s\n",(char *)buf);
    	return write_f(fd,buf,count);
	}

​ 此时被替换的read/write还未具有读写的功能,需要找到原始API函数地址:

	void init_hoot(void){ 
    	if(!read_f){
        	read_f = dlsym(RTLD_NEXT,"read");
    	}
    	if(!write_f){
        	write_f = dlsym(RTLD_NEXT,"write");
    	}
	}

​ dlsym(RTLD_NEXT,"read")找到API中系统调用的函数起点赋予read_f,从而实现了自定义替换read/write函数。

​ 在主函数中,按照正常的API调用即可:

	init_hoot();
    int fd = open("a.txt",O_CREAT | O_RDWR);
    if(fd < 0){
        return -1;
    }

    char *str = "1234567890";
    write(fd,str,strlen(str));

    char buffer[1128] = {0};
    read(fd,buffer,128);

​ 综上,ucontext方案基于ucontext_t的文本上下文控制,通过几个API进行协程切换,对于一些系统API通过Hook机制进行自定义。所有的协程资源的控制、互相的切换都基于scheduler进行操作。

  • 自定义汇编协程
  1. 协程的核心原语操作:create、yield、resume。

​ create用于创建一个协程,在函数中创建调度器实例并将实例作为线程的特定数据,存放在线程私有空间pthread_setspecific中;紧接着为coroutine分配内存空间,初始化用于存放上下文数据的栈等;最后将协程添加到就绪队列中。部分代码如下:

	assert(pthread_once(&sched_key_once, nty_coroutine_sched_key_creator) == 0);
	nty_schedule *sched = nty_coroutine_get_sched();
	
	nty_coroutine *co = calloc(1, sizeof(nty_coroutine));

	co->stack = NULL;
	co->stack_size = 0;
	co->sched = sched;
	co->status = BIT(NTY_COROUTINE_STATUS_NEW); //
	co->id = sched->spawned_coroutines ++;
	co->func = func;
	co->arg = arg;
	co->birth = nty_coroutine_usec_now();
	*new_co = co;

	TAILQ_INSERT_TAIL(&co->sched->ready, co, ready_next);

​ yield被程序主动调用让出执行CPU。在本文中提供了两种切换功能:swapcontext、_switch等操作,但无论是哪种操作其核心功能都是将当前的执行状态进行保存。若使用swapcontext,则需要显式保存栈;而自定义的_switch操作在汇编阶段即将数据保存。

void nty_coroutine_yield(nty_coroutine *co) {
	co->ops = 0;
#ifdef _USE_UCONTEXT
	if ((co->status & BIT(NTY_COROUTINE_STATUS_EXITED)) == 0) {
		_save_stack(co);
	}
	swapcontext(&co->ctx, &co->sched->ctx);
#else
	_switch(&co->sched->ctx, &co->ctx);
#endif
}

​ resume恢复被挂起的协程,从保存栈中提取数据恢复现场。

int nty_coroutine_resume(nty_coroutine *co) {
	
	...
#ifdef _USE_UCONTEXT	
	_load_stack(co);//恢复之前保存的栈内容
#endif
    //设置调度器上下文
	nty_schedule *sched = nty_coroutine_get_sched();
	sched->curr_thread = co;
    
#ifdef _USE_UCONTEXT
	swapcontext(&sched->ctx, &co->ctx);
#else
	_switch(&co->ctx, &co->sched->ctx);
	nty_coroutine_madvise(co);
#endif
	sched->curr_thread = NULL;
    
	...
        
}

​ 在上述操作中,最核心的内容就是协程的上下文切换。一个协程的上下文往往包括:寄存器状态、占空间、其他协定数据。切换需要完成的功能:保存当前协程的CPU上下文、恢复目标协程的CPU上下文、跳转到目标协程的执行点继续执行。

​ x86_64位架构的CPU提供了16个64位的通用寄存器:

寄存器 别名 主要用途 调用约定(System V ABI)
RAX EAX 累加器/返回值 返回值寄存器
RBX EBX 基址寄存器 被调用者保存
RCX ECX 计数器/第4参数 第4个参数
RDX EDX 数据/第3参数 第3个参数
RSI ESI 源索引/第2参数 第2个参数
RDI EDI 目标索引/第1参数 第1个参数
RBP EBP 基址指针 被调用者保存
RSP ESP 栈指针 被调用者保存
R8 - 第5参数 第5个参数
R9 - 第6参数 第6个参数
R10 - 临时寄存器 调用者保存
R11 - 临时寄存器 调用者保存
R12 - 通用寄存器 被调用者保存
R13 - 通用寄存器 被调用者保存
R14 - 通用寄存器 被调用者保存
R15 - 通用寄存器 被调用者保存

​ 其中%RSP指针指向栈顶;%RDI、%RSI、%RDX、%RCX、%r8、%r9分别对应函数的第1、2、3...个参数。%RBX、%RBP、%r12、%r13、%r14、%r15用于数据存储。所谓的协程切换就是将当前的寄存器值存储,将待执行的协程寄存器值赋予对应的寄存器。

​ 因此协程上下文结构体依据x86_64位架构定义:

typedef struct _nty_cpu_ctx {
	void *esp; //0栈指针
	void *ebp; //8帧指针
	void *eip; //16指令指针
	void *edi; //24
	void *esi; //32
	void *ebx; //40
	void *r1;  //48 r12
	void *r2;  //56 r13
	void *r3;  //64 r14
	void *r4;  //72 r15
	void *r5;  //80 保留
} nty_cpu_ctx;

​ switch操作包含了两步主要操作:保存当前上下文(%rsi)、恢复目标上下文(%rdi)。

	movq %rsp, 0(%rsi)      # 保存栈指针到ctx->esp
	movq %rbp, 8(%rsi)      # 保存帧指针到ctx->ebp
	movq (%rsp), %rax       # 从栈顶读取返回地址
	movq %rax, 16(%rsi)     # 保存返回地址到ctx->eip
	movq %rbx, 24(%rsi)     # 保存RBX到ctx->ebx
	movq %r12, 32(%rsi)     # 保存R12到ctx->r1
	movq %r13, 40(%rsi)     # 保存R13到ctx->r2
	movq %r14, 48(%rsi)     # 保存R14到ctx->r3
	movq %r15, 56(%rsi)     # 保存R15到ctx->r4

​ 恢复目标上下文:

	movq 56(%rdi), %r15     # 恢复ctx->r4到R15
	movq 48(%rdi), %r14     # 恢复ctx->r3到R14
	movq 40(%rdi), %r13     # 恢复ctx->r2到R13
	movq 32(%rdi), %r12     # 恢复ctx->r1到R12
	movq 24(%rdi), %rbx     # 恢复ctx->ebx到RBX
	movq 8(%rdi), %rbp      # 恢复ctx->ebp到RBP
	movq 0(%rdi), %rsp      # 恢复ctx->esp到RSP
	movq 16(%rdi), %rax     # 获取ctx->eip
	movq %rax, (%rsp)       # 将eip压入新栈顶
	ret                     # 跳转到eip
2. **协程的定义**

​ 作为协程的载体,协程的运行体必须包含运行状态、运行体回调函数、回调函数的参数、栈指针等参数。

typedef struct _nty_coroutine {

    nty_cpu_ctx ctx;
    proc_coroutine func;
    void *arg;
    size_t stack_size;

    nty_coroutine_status status;
    nty_schedule *sched;

    uint64_t birth;
    uint64_t id;

    void *stack;

    RB_ENTRY(_nty_coroutine) sleep_node;
    RB_ENTRY(_nty_coroutine) wait_node;

    TAILQ_ENTRY(_nty_coroutine) ready_next;
    TAILQ_ENTRY(_nty_coroutine) defer_next;

} nty_coroutine;

​ 新被创建的协程被放入就绪集合中,当协程被调用但因为其中的操作并未执行结束(例如某一协程用于IO操作,但该操作一时间无法得到返回值)此时协程处于等待状态,当协程操作执行完毕进入睡眠状态。基于上述特性分别使用队列存储就绪协程、使用红黑树存储等待和睡眠集合。

​ 调度器是用于管理所有协程运行的组件,它必须包含协程上下文用于不同协程之间进行切换。除此之外还需要包含协程三种状态的数据结构,以及用于事件管理的epoll相关组件。

typedef struct _nty_schedule {
	uint64_t birth;
	nty_cpu_ctx ctx;

	void *stack;
	size_t stack_size;
	int spawned_coroutines;
	uint64_t default_timeout;
	struct _nty_coroutine *curr_thread;
	int page_size;

	int poller_fd;
	int eventfd;
	struct epoll_event eventlist[NTY_CO_MAX_EVENTS];
	int nevents;

	int num_new_events;
	pthread_mutex_t defer_mutex;

	nty_coroutine_queue ready;
	nty_coroutine_queue defer;

	nty_coroutine_link busy;
	
	nty_coroutine_rbtree_sleep sleeping;
	nty_coroutine_rbtree_wait waiting;

} nty_schedule;

​ 在上述结构体基础上协程有两种被调度的方式:

  • 生产者消费者模式

​ 将已经到期的睡眠协程作为生产者,放入就绪队列中。

	nty_coroutine *expired = NULL;//睡眠集合
	while ((expired = sleep_tree_expired(sched)) != NULL) {
    	TAILQ_ADD(&sched->ready, expired);
	}
	
	nty_coroutine *wait = NULL;//等待集合
        int nready = epoll_wait(sched->epfd, events, EVENT_MAX, 1);
        for (i = 0;i < nready;i ++) {
            wait = wait_tree_search(events[i].data.fd);
            TAILQ_ADD(&sched->ready, wait);
    }

​ 调度器作为消费者从就绪队列中取出协程执行。

	while (!TAILQ_EMPTY(&sched->ready)) {
            nty_coroutine *ready = TAILQ_POP(sched->ready);
            resume(ready);
    }
  • 多状态运行

​ 这种方案综合调度三种不同状态:睡眠状态、等待状态、就绪状态。调度器在这其中协调控制在合适的时机执行这些函数。

​ 睡眠状态:

	nty_coroutine *expired = NULL;
	while ((expired = sleep_tree_expired(sched)) != NULL) {
    	resume(expired);
	}

​ sleep_tree_expired从睡眠集合中查找定时器已到期的协程,进而resume恢复到期的协程执行权。

​ 等待状态:

	int nready = epoll_wait(sched->epfd, events, EVENT_MAX, 1);
	for (i = 0; i < nready; i++) {
    	wait = wait_tree_search(events[i].data.fd);
    	resume(wait);
	}

​ epoll_wait等待IO事件并将超时时间设为1ms,根据就绪的fd在等待集合中找到对应协程交付给resume恢复执行权。

​ 就绪状态:

	while (!TAILQ_EMPTY(sched->ready)) {
    	nty_coroutine *ready = TAILQ_POP(sched->ready);
    	resume(ready);
	}

​ 在该状态只需要简单地从就绪队列中取出协程执行即可。

三、基于协程的网络API

​ 将非阻塞网络IO和事件驱动与协程结合,在少量线程的基础上处理大量的请求。与协程组合的关键在于Hook操作,将系统API(socket、accept、recv等)替换为协程版本。

  • 协程的创建与初始化

​ nty_socket创建套接字并设置为非阻塞模式,在Hook机制下,socket系统调用被替换为协程版本。在socket中关联调度器。

int nty_socket(int domain, int type, int protocol) {

	int fd = socket(domain, type, protocol);
	int ret = fcntl(fd, F_SETFL, O_NONBLOCK);
	int reuse = 1;
	setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));
	return fd;
}

socket_t socket_f = NULL;

int socket(int domain, int type, int protocol) {

	if (!socket_f) init_hook();

	nty_schedule *sched = nty_coroutine_get_sched();
	if (sched == NULL) {
		return socket_f(domain, type, protocol);
	}
    
	int fd = socket_f(domain, type, protocol);
	int ret = fcntl(fd, F_SETFL, O_NONBLOCK);
	int reuse = 1;
	setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));
	return fd;
}
  • 网络IO操作

​ 以recv为例:当recv阻塞时,使用nty_poll_inner将当前协程加入epoll监听,使用nty_coroutine_yeild让出控制权。当IO事件就绪时唤醒对应协程重新执行recv。

ssize_t nty_recv(int fd, void *buf, size_t len, int flags) {
	
	struct pollfd fds;
	fds.fd = fd;
	fds.events = POLLIN | POLLERR | POLLHUP;

	nty_poll_inner(&fds, 1, 1);

	int ret = recv(fd, buf, len, flags);
	return ret;
}

recv_t recv_f = NULL;

ssize_t recv(int fd, void *buf, size_t len, int flags) {

	if (!recv_f) init_hook();
	nty_schedule *sched = nty_coroutine_get_sched();
	if (sched == NULL) {
		return recv_f(fd, buf, len, flags);
	}
    
	struct pollfd fds;
	fds.fd = fd;
	fds.events = POLLIN | POLLERR | POLLHUP;

	nty_poll_inner(&fds, 1, 1);

	int ret = recv_f(fd, buf, len, flags);
	return ret;
}
  • 连接处理

​ 仍然循环监听POLLIN事件,如有新连接加入,accpet以非阻塞方式获取连接。

int nty_accept(int fd, struct sockaddr *addr, socklen_t *len) {
	int sockfd = -1;
	int timeout = 1;
	nty_coroutine *co = nty_coroutine_get_sched()->curr_thread;
	
	while (1) {
		struct pollfd fds;
		fds.fd = fd;
		fds.events = POLLIN | POLLERR | POLLHUP;
		nty_poll_inner(&fds, 1, timeout);

		sockfd = accept(fd, addr, len);
		if (sockfd < 0) return -1;
		else            break;
	}

	int ret = fcntl(sockfd, F_SETFL, O_NONBLOCK);
	int reuse = 1;
	setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));
	return sockfd;
}

accept_t accept_f = NULL;

int accept(int fd, struct sockaddr *addr, socklen_t *len) {

	if (!accept_f) init_hook();

	nty_schedule *sched = nty_coroutine_get_sched();
    if (sched == NULL) {
		return accept_f(fd, addr, len);
	}
    
	int sockfd = -1;
	int timeout = 1;
	nty_coroutine *co = nty_coroutine_get_sched()->curr_thread;
	
	while (1) {
		struct pollfd fds;
		fds.fd = fd;
		fds.events = POLLIN | POLLERR | POLLHUP;
		nty_poll_inner(&fds, 1, timeout);

		sockfd = accept_f(fd, addr, len);
		if (sockfd < 0) return -1;
		else            break;
	}

	int ret = fcntl(sockfd, F_SETFL, O_NONBLOCK);
	int reuse = 1;
	setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));
	
	return sockfd;
}

​ 其余类似功能实现不再赘述。

问题与解决方案

能否解释什么是异步,举出常见应用中用到异步操作的十种场景?

​ 如果顾客点餐取号后一直在前台等待到出餐后才能离开,即顾客此时被阻塞在前台。在客流量大的场景下,未取号的顾客只有一直等待未取餐的顾客。在这个场景下就是同步,所有前继的操作结束后才能执行后续操作。

​ 如果顾客点餐取号后可以立即离开柜台去做自己的事情(执行其他任务),自己的点餐完成后前台通知(回调通知),此时顾客去取自己的餐(处理结果)。在这个过程中顾客没有把时间花在没有必要的等待上,可以同时做其他的事情。这种机制就是异步。

​ 常见应用场景的异步操作:

  1. 网页请求数据:浏览器作为客户端向服务器请求数据,如果该网络交互速度较慢发送和回应耗时无法预测,采用同步等待浏览器就会在此处卡死,用户无法进行任何操作,大大降低用户体验;一般采用回调函数等操作。
  2. 用户界面交互:主线程一直监听用户操作(点击按钮,输入文本等),如果某一事件处理及其耗时也会影响用户体验;采用事件监听器等机制。
  3. 服务器处理请求:web服务器接收客户端HTTP请求。如果服务器采用一条主线程,如果客户端HTTP请求需要访问其余慢速IO(例如读写磁盘等),会造成服务器线程阻塞,后续所有请求都无法处理;多采用io_uring、协程等异步机制。
  4. 消息队列系统:生产者将消息发送到队列,消费者如果同步等待消费者,会造成资源浪费线程阻塞;采用异步机制将生产者消费者解耦,双方可以按照自己的节奏处理消息。
  5. 文件读写:应用程序读写本地磁盘文件,IO操作相对CPU来说十分缓慢,采用同步读写会大大拖累CPU效率;采用异步读写机制当IO就绪后中断通知CPU处理,将CPU从缓慢的IO操作中解放出来。
  6. 移动网络应用请求:在无线移动网路场景下,网络传输速率波动较大。App若采用同步请求,会导致用户界面阻塞严重影响用户体验。
  7. 推送通知:服务器向客户端推送实时推送通知,在大量客户端的场景下不能阻塞等待每个客户端响应;一般采用全双工通信、消息队列等解决方案。
  8. 定时任务:指定某个函数在指定时间后执行,此时主线程应该转而去执行后续代码,若同步等待会大大降低代码运行效率。
  9. 数据库操作:当数据库查询涉及到本地硬件的访问时,同步会导致整个应用程序阻塞;一般采用数据库驱动提供的异步操作接口。
  10. 游戏场景:在游戏中每帧场景都需要渲染画面,在追求高帧率的目的下,加载资源等操作处理上事件较长必须异步执行。

总结

​ 基于对协程原理及网络IO协程框架的深入分析,本文系统性地阐释了协程作为用户态轻量级线程的核心价值:通过主动让出(yield)和恢复(resume)的执行权切换机制,在保持同步编程直观性的同时实现异步并发的性能。文章依次剖析了三种协程实现方案(setjmp/longjmp基础原语、ucontext上下文管理、自定义汇编级高性能切换),重点构建了基于调度器、协程控制块及事件驱动的协程框架,并通过Hook机制重写网络IO系统调用(如socket/recv/accept),使阻塞操作转化为非阻塞的协程切换,为高并发服务开发提供了兼具简洁性与性能的解决方案。

posted @ 2025-06-14 11:16  +_+0526  阅读(34)  评论(0)    收藏  举报