协程库——面试问题

0 简单介绍项目

项目描述:
本项目是在Linux环境下使用C++开发的一个协程库,适合socket IO密集型任务的处理。协程基于ucontext_t实现,使用非对称协程模型。协程调度器支持协程的调度以及socket IO事件/定时事件的回调。同时,对常见系统API进行hook,实现异步效果。
主要工作:
封装:基于RAII思想,封装了pthread_mutex_t、pthread_rwlock_t,实现范围锁;
协程实现:基于ucontext_t实现协程类,使用非对称协程模型,每个协程使用独立栈空间。协程设计了READY/RUNNING/TERM三种状态;协程之间通过 resume/yield进行切换;
定时事件:基于时间堆实现定时器,支持取消、刷新和重置定时器;
IO事件:封装[fd-fd注册的事件-事件的回调函数]为FdContext类,支持添加、取消和删除事件;
协程调度:支持普通协程的调度和IO事件/定时事件的回调。调度器内部实现 调度线程池和任务队列,调度线程循环从任务队列中取出任务然后进行调度,当任务队列为空时,调度线程通过epoll_wait阻塞在idel协程中,idel协程负责在事件发生/定时器超时后,将回调函数添加到任务队列中;
协程池:每个调度线程中分别创建一个协程池,调度时,将任务绑定到协程池中空闲协程上进行调度;
hook模块:hook系统底层socket相关、socket IO相关以及sleep系列的API。若系统阻塞则自动注册IO事件或定时事件,然后让出cpu,当事件发生或定时器超时再继续执行,从而使一些不具备异步功能的API,展现出异步的效果。
测试:使用原生epoll和本项目分别编写简单服务器,使用ApacheBench进行压力测试,单线程条件下本项目没有性能损失,在多线程、大流量、IO密集条件下,本项目有一定的性能提升。

什么是异步效果?

        为fd注册读事件后,直接返回,执行其他任务。事件发生后,回调函数会被自动调度,进行read操作。对于用户来说,读的过程不需要阻塞等待。

基于简历的问题

1 RAII

Resource Acquisition is Initialization,资源获取即初始化。

使用类来管理资源,在构造函数中申请分配资源,在析构函数中释放资源,将资源和对象的生命周期绑定。智能指针是RAII最好的例子。

范围锁:基于RAII,在构造函数中上锁,在析构函数中解锁。

2 锁

2.1 项目中哪里使用锁?

        工作线程从任务队列中取任务,需要互斥锁(比读写锁更安全)

        读写全局变量,使用读写锁

2.2 锁的种类

互斥锁

读写锁

自旋锁:循环等待获取锁

信号量、条件变量

数据库中的锁:

  • 悲观锁:假定访问数据时冲突才是常态,访问数据之前就上锁,防止其它线程的操作,直到当前事务结束。适合写多读少的场景。
  • 乐观锁:假定访问数据时冲突较少,访问数据时不加锁,而是在更新数据之前检查数据是否被其它事务更改过,只有数据未修改,当前操作才会成功。
  • 行锁、表锁、共享锁、排他锁
2.3 为什么不用c11的线程和锁

std::thread也是基于pthread实现的,但是在c11中没有读写锁,这在多线程开发中需要频繁使用,所以选择自己封装。

tips:c++14中提供了读写锁。

c++11中的锁:

  • std::lock_guard:对象创建,自动上锁,对象析构,自动解锁。
  • std::unique_lock:在lock_guard的基础上,用户可以手动上锁和解锁。

3 ucontext_t接口

4 对称协程、非对称协程 

5 有栈协程、无栈协程

6 独立栈、共享栈

7 为什么只有三种状态

  • 模型更加简洁,易于管理和实现。
  • 降低维护成本,减少了出错的可能。

8 定时器的实现方式

9 线程池的实现

本项目中,调度器包含调度线程和任务队列,调度器中的线程池是一个线程数组,调度器本身行使了传统线程池的任务。

传统的线程池包含以下几个要素:

  • 工作线程
  • 任务队列
  • 线程同步

c++11线程池示例

#include<queue>
#include<thread>
#include<mutex>
#include<conditon_variable>
#include<functional>
#include<vector>

class ThreadPool {
public:
	ThreadPool(size_t threadCount) : stop(false) {
		for (size_t i = 0; i < threadCount; ++i) {
			//根据参数,调用对应的构造函数
			workers.emplace_back([this] {
				//工作函数的逻辑
				while (true) {
					std::function<void()> task;
					{
						std::unique_lock<std::mutex> lock(queueMutex);
						//释放lock;被唤醒后,也要匿名函数返回true,才能继续执行,否则继续等待
						condition.wait(lock, [] {return stop || !task.empty(); });
						if (stop && task.empty())
							return;
						//移动
						task = std::move(tasks.front());
						tasks.pop();
					}
					task();
				}
			});
		}
	}

	~Thread_pool() {
		{
			std::unique_lock<std::mutex>lock(queueMutex);
			stop = true;
		}
		condition.notify_all();
		for (std::thread& worker : workers) {
			worker.join();
		}
	}

	void enqueue(std::function<void()> f) {
		{
			std::unique_lock lock(queueMutex);
			task.emplace_back(std::move(f));
		}
		condition.notify_one();
	}

private:
	std::vector<std::thread> workers;
	//返回类型void,()中是参数类型,无参数
	std::queue<std::function<void()>> tasks;
	std::mutex queueMutex;
	std::condition_vairable condition;
	bool stop;
};

void printHello(int id) {
	std::cout << "Hello from thread " << id << std::endl;
}

int main() {
	ThreadPool pool(4);
	for (int i = 0; i < 10; ++i) {
		pool.enqueue([i] {printHello(i); });
	}
	return 0;
};

10 hook的作用,怎样实现?

hook将原来的api进一步封装,在执行真正的系统调用之前,执行一些隐藏的操作,从而实现异步的效果,如下:        

        hook socket IO,read/write:如果暂时无法读/写,则为fd注册对应事件,yield,等待事件发生后,再把这个协程添加到调度队列。

        用户不需要使用epoll进行监测,可使用顺序编程的方法,实现异步的效果。IO调用返回后,IO就已经完成,中间过程不需要用户参与。

int fd = accept()
read(fd)
...
write(fd)
...

其它hook: 

  • socket:如果用户没有设置为阻塞模式,则设置fd为非阻塞
  • sleep:注册定时事件,然后yield,超时后,再把这个协程添加到调度队列

11 用了什么设计模式?

单例模式。

创建fd管理类FdCtx,标识fd是否阻塞,是否是socket fd,是否关闭;

创建FdCtx集合类FdManager管理所有FdCtx,FdManager使用单例模式。

为什么要使用单例模式?

        主要是为了提供一个全局访问点,方便访问;保证全局仅有一份实例,节约内存资源;避免多个实例产生冲突。

        FdManager是全局使用的,并不是某个类的成员,似乎可以使用全局变量来实现。但是:

  • 全局变量可能会被拷贝,导致内存中出现多个实例,浪费内存;
  • 遵循设计模式,代码更规范。

注意:单例模式本身无法避免线程同步问题,需要使用互斥锁等

        

       

单例模式的优点:

http://t.csdnimg.cn/3m8aD

1 IO模型

1.1 同步

1.1.1 阻塞IO

 用户执行read,会阻塞等待数据准备好、数据从内核拷贝到应用进程两个过程,拷贝完成,read才会返回。

1.1.2 非阻塞IO

read在数据未准备好时,立即返回,并设置错误码 EAGAIN/ EWOULDBLOCK,此时应用程序不断轮询内核,直到数据准备好,然后将数据从内核拷贝到用户缓冲区中,read返回。

read最后一次,需要等待数据从内核拷贝到用户缓冲区,这是同步的过程。

1.1.3 IO多路复用

 I/O 多路复用接口最大的优势在于,用户可以在一个线程内同时处理多个 socket 的 IO 请求

同样,read需要等待数据从内核拷贝到用户缓冲区,这是同步的过程。

1.2 异步

异步IO,内核数据准备好、数据从内核拷贝到用户态,都不需要等待。 

用户调用aio_read后,立即返回,内核自动准备数据并拷贝到用户态,不需要等待,这是个异步的过程。拷贝完成后,通知用户。

1.3 总结

同步IO一定会阻塞在【过程2】,而异步IO不会阻塞。 

2 进程、线程、协程

进程是操作系统进⾏资源分配的基本单位,每个进程都有⾃⼰的独⽴内存空间;

线程是cpu调度的基本单位,线程共享父进程的虚拟地址空间;

协程是用户态线程,协程通常在线程中运行

2.1 切换上下文

进程

cpu上下文——寄存器

虚拟内存相关——页表

资源相关——文件句柄

线程

线程切换不需要切换虚拟地址空间,

只需要切换cpu寄存器上下文和少量的资源管理上下文

协程

部分cpu寄存器,

当前调用栈栈基地址代码的执行位置等,当前的上下文保存到线程的堆区

2.2 多进程/线程/协程

多进程

fork()创建子进程,子进程拷贝父进程地址空间,写时复制,代码段相同,执行任务相同

exec 系列函数可以在子进程中加载新的可执行程序,将子进程的代码替换为新程序的代码。这样,子进程将执行与父进程不同的任务。

多线程

父进程创建多个线程,每个线程有自己的入口函数,执行不同的任务。多个线程共享父进程的资源。

协程

每个协程由自己的入口函数,执行不同的任务。

协程通常是在单线程中运行的,协程可以在线程中实现切换,开销比线程和进程切换小,可以实现高并发。

协程经常与多线程一起使用。

3 协程优缺点

协程优点

轻量级,创建和销毁开销小;

占用资源少,同一时间可以维持更多的并发单元,提高程序的并发性能

相对于线程,协程上下文切换开销小,速度快,在高并发IO密集场景下,能显著提升系统的吞吐量和响应时间

用户使用同步方式编程,就可以实现异步的效果,简化编程的复杂性。参考10hook

缺点

⽆法利⽤多核资源:线程才是系统调度的基本单位,单线程下的多协程本质上还是串⾏执⾏的,只能⽤到单核计算资源,所以协程往往要与多线程、多进程⼀起使⽤。 

难以调试:由于协程的切换和异步执行,调试协程代码可能更加困难。当协程之间存在复杂的依赖关系和交互时,追踪问题的根因可能变得复杂。

4 协程适用于I/O密集型任务的原因

非阻塞IO条件下,等待IO的过程中,会切换到其它任务继续执行:

        相比线程,协程切换快速,开销小;

        协程轻量级,占用资源少,可以创建大量协程,处理并发,不会像线程一样收到资源的限制。

5 协程实现的是真正的异步吗?

底层使用的是同步非阻塞IO,还是同步的,但是可以实现异步的效果,调用返回后,数据就已经读取完成,期间,不需要用户干预。

衡量⼀个协程库性能的标准

响应时间

吞吐量

并发能力:同时处理的协程数量

上下文切换开销

资源利用率:cpu,内存,网络等。可以在资源利用的方面进行优化,从而提升性能。

7 Go协程

Go从语言层面支持协程,Goroutine就是Go中最基本的执行单元。每一个Go程序至少有一个Goroutine,从main函数开始,Go程序会为main函数创建一个默认的Goroutine。

8 C++协程

是c++20引入一种语言新特性,通过co_await和co_yield实现

9 为什么要有空闲协程

在任务队列为空时,阻塞在idel协程中的epoll_wait中。idel协程负责使用epoll监听事件,实际发生后,将对应回调函数添加到调度队列中。

调度协程只负责任务调度,idel协程负责添加任务,这样,降低了不同功能之间的耦合,便于后序扩展和维护。

10 每建⽴⼀个⽤户连接就要创建⼀个协程,不会影响性能吗?

 会的,高并发时,会有大量的协程创建和销毁,会占用较多系统资源。

可使用协程池的方法解决。提前创建一定数量的协程,有新的任务时,直接复用已有的协程。

11 测试+优化

11.1 测试

测试方法:使用原生epoll和本项目的协程库,分别在单线程和多线程条件下,编写服务端程序。服务端接收到请求后,回复一个简单的页面。

测试工具:apachebench(ab)

结论:        

  • 单线程情况下,相比于直接使用epoll,性能并无太大差异。
  • 多线程情况下,在IO密集、线程或协程切换情况较多时,使用协程有明显的性能优势。

11.2 优化——协程池

每个调度线程中,创建一个协程池。

开始调度之前,先创建一定数量的协程;

调度时,选择空闲的协程绑定任务进行调度。如果没有空闲的协程,则创建一个新的协程,可以选择是否添加进入协程池。

协程池的存在,避免了部分协程的创建和析构,在一定程度上提升了系统的性能。

协程池中协程的数量选择:

如果不需要等待IO,或任务执行的过程中,不需要yield:因为在线程中,协程是串行执行的,执行完一个,再执行另一个。同一时间,只有一个协程在执行,且没有处于挂起状态的协程(本项目中是ready状态),那么,协程池中只需要一个协程就足够,提升协程的数量不能提升性能;

如果,需要等待IO,或任务执行的过程中,需要yield:同一时刻,有大量被挂起的协程,还有一个正在执行的协程,那么,不考虑内存影响,协程池的协程数量越多,性能提升越大。为了简单,本项目选择创建新的协程,并且新的协程不添加入协程池。

12 困难

12.1 协程的调度

首先需要考虑协程的切换:

本项目使用非对称协程模型,子协程只能和调度协程切换,调度协程再选择新的子协程进行调度。子协程不能直接resume子协程。

然后是调度器的设计:

调度器包含调度线程池和任务队列。调度线程包含两个主要协程,分别是调度协程和idel协程。任务队列非空时,调度协程从任务队列中取任务进行调度;调度队列为空时,调度协程切换到idel协程, 阻塞在epoll_wait,释放cpu,避免忙等。IO事件发生后,idel协程负责将任务添加到任务队列。

调度器停止:

需要等待任务队列为空,并且所有调度线程都执行完毕

12.2 hook

对IO系统调用进行封装,如果阻塞,则为fd注册对应事件,当事件发生后,将回调函数添加到任务对队列中进行调度。这样,用户可以使用同步的编程方式,实现异步的效果。

13 收获 

深入了解了协程,熟悉独立栈/共享栈,对称/非对称协程的概念

对进行、线程加深了理解

了解了Linux网络编程,了解IO多路复用、事件驱动模型。

14 其它协程库 

c++20协程

go协程

Boost.Coroutine2

libco:腾讯

旧版本简历

项目描述:
本项目在Linux环境下使用C++开发了一个协程库,适合socket IO密集型任务的处理。协程基于ucontext_t实现,使用非对称协程模
型。结合epoll和定时器实现了协程调度器,支持cpu任务的调度以及socket IO事件、定时事件的回调。同时,对常见系统API进行
hook,实现异步效果。
主要工作:
锁的封装:基于RAII思想,封装了pthread_mutex_t、pthread_rwlock_t;
协程实现:基于ucontext_t实现了非对称协程,每个协程有独立栈空间。协程有READY/RUNNING/TERM三种状态,协程使用
resume/yield来获取/让出cpu;
N-M协程调度器:调度器内部实现一个调度线程池和任务队列(存放待调度的协程),调度线程循环从任务队列中取出任务然后进
行调度。当任务队列为空时,调度线程进入idel状态,等待新的调度任务。调度器支持使用main函数所在线程进行调度;
IO协程调度器:继承自N-M协程调度器,封装了epoll,支持注册socket fd读/写事件的回调。当任务队列为空时,调度线程阻塞
在epoll_wait上,当注册的IO事件发生或添加了新的调度任务时再返回;
定时器:基于时间堆实现,通过定时器可以给服务器注册定时事件。定时器超时,则将定时器的回调函数添加到任务队列中进行调
度;
Hook模块:基于IO协程调度器和定时器实现,hook系统底层socket相关、socket IO相关、以及sleep系列的API。系统阻塞则注
册IO事件或定时事件,然后让出cpu,当事件发生或定时器超时再返回,从而使一些不具备异步功能的API,展现出异步的性能。

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据(含vue前端源码).zip 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!有问题请及时沟通交流。 2、适用人群:计算机相关专业(如计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、自动化、电子信息等)在校学生、专业老师或者企业员工下载使用。 3、用途:项目具有较高的学习借鉴价值,不仅适用于小白学习入门进阶。也可作为毕设项目、课程设计、大作业、初期项目立项演示等。 4、如果基础还行,或热爱钻研,亦可在此项目代码基础上进行修改添加,实现其他不同功能。 欢迎下载!欢迎交流学习!不清楚的可以私信问我! 毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据(含vue前端源码).zip
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值