从零开始实现一个C++高性能服务器框架----协程调度模块

此项目是根据sylar框架实现,是从零开始重写sylar,也是对sylar丰富与完善
项目地址:https://gitee.com/lzhiqiang1999/server-framework

简介

项目介绍:实现了一个基于协程的服务器框架,支持多线程、多协程协同调度;支持以异步处理的方式提高服务器性能;封装了网络相关的模块,包括socket、http、servlet等,支持快速搭建HTTP服务器或WebSokcet服务器。
详细内容:日志模块,使用宏实现流式输出,支持同步日志与异步日志、自定义日志格式、日志级别、多日志分离等功能。线程模块,封装pthread相关方法,封装常用的锁包括(信号量,读写锁,自旋锁等)。IO协程调度模块,基于ucontext_t实现非对称协程模型,以线程池的方式实现多线程,多协程协同调度,同时依赖epoll实现了事件监听机制。定时器模块,使用最小堆管理定时器,配合IO协程调度模块可以完成基于协程的定时任务调度。hook模块,将同步的系统调用封装成异步操作(accept, recv, send等),配合IO协程调度能够极大的提升服务器性能。Http模块,封装了sokcet常用方法,支持http协议解析,客户端实现连接池发送请求,服务器端实现servlet模式处理客户端请求,支持单Reator多线程,多Reator多线程模式的服务器。

协程调度模块

  • 协程模块中,我们只能处理主协程与子协程间的切换,无法实现在子协程中调用调用协程,在协程调度模块中可以解决这个问题。子协程可以通过向调度器添加调度任务的方式来运行另一个子协程。
  • 创建一个协程调度器,然后把要调度的协程传递给调度器,由调度器负责把这些协程一个一个消耗掉。这里给出了一个协程调度器的简单实现版本
/**
 * @brief 简单协程调度类,支持添加调度任务以及运行调度任务
 */
class Scheduler {
public:
    /**
     * @brief 添加协程调度任务
     */
    void schedule(sylar::Fiber::ptr task) {
        m_tasks.push_back(task);
    }
 
    /**
     * @brief 执行调度任务
     */
    void run() {
        Fiber::ptr task;
        auto it = m_tasks.begin();
 
        while(it != m_tasks.end()) {
            task = *it;
            m_tasks.erase(it++);
            task->call();
        }
    }
private:
    /// 任务队列
    std::list<sylar::Fiber::ptr> m_tasks;
};

// 协程任务
void test_fiber(int i) {
    std::cout << "hello world " << i << std::endl;
}
 
int main() {
    /// 初始化当前线程的主协程
    Fiber::GetThis();
 
    /// 创建调度器
    Scheduler sc;
 
    /// 添加调度任务
    for(auto i = 0; i < 10; i++) {
        Fiber::ptr fiber(new Fiber(
            std::bind(test_fiber, i)
        ));
        sc.schedule(fiber);
    }
 
    /// 执行调度任务
    sc.run();
 
    return 0;
}

1. 主要功能

  • 实现了一个线程池,支持多线程,多协程协同调度
  • 能够支持将主线程作为调度线程,提高程序运行效率
  • 支持将函数或协程绑定到一个具体的线程上执行

2. 功能演示

void task2(){
	// 任务2
}

void task1() {
	// 任务1
	Scheduler::GetThis().schedule(task2);	// 任务协程中也能添加任务
}

int main(){
	// 创建协程调度器,并使用主线程作为调度线程
	Scheduler sc(1, true, "name");
	sc.schedule(task1);
	sc.start();
	sc.stop();
	
	return 0;
}

3. 模块介绍

3.1 Scheduler

  • 协程调度器,包含了线程池,协程任务队列,调度协程等
class Scheduler
{
public:
	typedef std::shared_ptr<Scheduler> ptr;
	typedef Mutex MutexType;

public:
	Scheduler(size_t threads = 1, bool use_caller = true, const std::string& name = "");
	virtual ~Scheduler();
	
	void schedule(FiberOrCb fc, int thread = -1);			//添加任务协程
	void schedule(InputIterator begin, InputIterator end);
public:
		
		static Scheduler* GetThis();			// 获取当前协程调度器
		static void SetThis(Scheduler* s);		// 设置当前协程调度器
		static Fiber* GetScheduleFiber();		// 获取当调度协程
private:
		struct FiberAndCallBack{ ... };
protected:	
	void run();					// 协程调度函数
	virtual void idle();		// 协程无任务可调度时执行idle协程
	virtual bool stopping();	// 返回是否结束
	virtual void tickle();		// 唤醒协程

private:
	MutexType m_mutex;						//锁
	std::vector<Thread::ptr> m_threads;		//线程池
	std::list<FiberAndCallBack> m_fibers;	//即将执行和计划执行的协程,由协程完成具体任务
	Fiber::ptr m_schedulFiber;				//调度协程,只在use_caller = true有效
	std::string m_name;						//调度器名称

protected:
	std::vector<int> m_threadIds;			//协程下的线程id数组
	int m_rootThread = 0;					//主线程id(use_caller = true才使用)
	size_t m_threadCount = 0;				//线程池中线程数量
	bool m_stopping = true;					//是否正在停止
	bool m_autoStop = false;				//是否自动停止
	 
	std::atomic<size_t> m_activeThreadCount = { 0 };	//工作线程数量
	std::atomic<size_t> m_idleThreadCount = { 0 };		//空闲线程数量
};

3.2 任务协程

  • 对于协程调度器来说,协程可以是调度任务,但实际上,函数也可以是,只需要把函数包装成协程即可。因此这里设计了一个调度任务FiberAndCallBack
struct FiberAndCallBack
{
	Fiber::ptr fiber;			// 任务协程
	std::function<void()> cb;	// 函数
	int thread;					// 线程id
	
	FiberAndCallBack(Fiber::ptr f, int thr)
		:fiber(f),
		thread(thr)
	{}

	FiberAndCallBack(Fiber::ptr* f, int thr)
		:thread(thr)
	{
		fiber.swap(*f);
	}

	FiberAndCallBack(std::function<void()> f , int thr)
		:cb(f),
		thread(thr)
	{}

	FiberAndCallBack(std::function<void()>* f, int thr)
		:thread(thr)
	{
		cb.swap(*f);
	}

	FiberAndCallBack()
		:thread(-1)
	{}

	void reset()
	{
		fiber = nullptr;
		cb = nullptr;
		thread = -1;
	}
};

3.3 线程池与调度线程

  • 在协程调度模块,我们说单线程同时刻只能运行一个协程。这样效率明显不高。调度器需要用多线程来提高效率,这样就能让多个协程同时执行。
vector<Thread::ptr> m_threads;		//线程池

3.4 让主线程也充当调度线程

  • 为了提高效率,可以选择将主线程也充当调度协程。比如,在main函数中定义了调度器,可以把main函数所在线程也用来执行调度任务。使用use_caller来控制。
// use_caller: true 使用主线程; false 不使用主线程
Scheduler(size_t threads = 1, bool use_caller = true, const std::string& name = "");

use_caller为true时,创建的协程应当在主协程 和 子协程(调度协程)间交换(call、back)。当use_caller为false时,调度协程 和 调度协程的子协程交换(swapIn、swapOut)。后文还会详细说明。

call/back:		专门负责调度协程和主协程间转换
swapIn/swapOut:专门负责调度协程和任务协程间转换
         call          swapIn
主协程<--->调度协程<--->任务协程
    back      swapOut

3.5 添加任务协程

  • 当在协程中需要添加任务协程时,可以执行调度器的schedule方法
void task() {
	// 任务协程的逻辑处理
}

Fiber::ptr fiber(task)
Scheduler sc;
sc.schedule(fiber);
  • 每次添加完都需要执行tickle方法,告诉其他调度线程有新的任务到来

3.6 如何调度任务

  • 所有调度线程按顺序从任务协程队列中取任务执行,当没有任务时,执行idle协程,等待新的任务到来
// 创建线程池,每个调度线程都注册了一个调度器的执行函数
for (size_t i = 0; i < m_threadCount; ++i)
{
	m_threads[i].reset(new Thread(std::bind(&Scheduler::run, this), m_name + "_" + std::to_string(i)));
	m_threadIds.push_back(m_threads[i]->getId());//这里需要注意,当new Thread的时候,我们wait了一下,等线程执行函数初始化完再notify,这样就保证了能拿到此处的线程id;
}
  • 调度协程依次查看任务协程队列有无任务
Fiber::ptr idle_fiber(new Fiber(std::bind(&Scheduler::idle, this)));//闲置协程
Fiber::ptr cb_fiber;//使用回调函数的协程

while(true) {
	auto it = m_fibers.begin();
	while (it != m_fibers.end())
	{
		//当前协程任务(FiberAndThread)中设置的线程id != 当前线程id(我们指定了每个协程任务(FiberAndThread)应该在哪里跑)
		if (it->thread != -1 && it->thread != johnsonli::getThreadId())
		{
			++it;
			tickle_me = true;
			continue;
		}

		DO_ASSERT(it->fiber || it->cb);
		//协程任务(FiberAndThread)中的协程目前正在EXEC状态,不执行
		if (it->fiber && it->fiber->getState() == Fiber::EXEC) {
			++it;
			continue;
		}

		//找到了一个可以执行的协程任务(FiberAndThread)
		fc = *it;
		m_fibers.erase(it++);//获取当前协程任务(FiberAndThread)后,从协程队列中移除
		++m_activeThreadCount;//增加一个线程执行
		is_active = true;//当前线程存活
		break;
	}

	tickle_me |= (it != m_fibers.end());//如果还没有遍历完,就可以通知调度器有任务
	if (tickle_me) { tickle(); //通知调度器有任务 }
	if (fc.fiber && (fc.fiber->getState() != Fiber::TERM
				&& fc.fiber->getState() != Fiber::EXCEPT)) {
		fc.fiber->swapIn(); //执行任务协程
		....
	}else if (fc.cb) { //协程任务有回调函数
		cb_fiber.reset(new Fiber(fc.cb));
		fc.reset();
		cb_fiber->swapIn();	//执行任务协程
	}
	else {
		idle.swapIn(); //执行idle协程
	}
	
}
  • idle协程需要轮询等待,新的任务协程到来
void Scheduler::idle() { 

	//如果没结束,就切换成HOLD(暂停)状态,这样就不会退出当前线程
	//这里要使用while,因为下一次swapIn,还会再判断一次
	while(!stopping()) {
		Fiber::YieldToHoldBySwap();
	}
}

3.7 调度器停止

  • 调度器应该支持停止调度的功能,以便回收调度线程的资源,只有当所有的调度线程都结束后,调度器才算真正停止
void Scheduler::stop() {
	m_autoStop = true;

	//调度协程!=null && 调度协程状态=TERM(已经完成) | INIT(还未开始) && 当前线程池为null
	if (m_schedulFiber
		&& (m_schedulFiber->getState() == Fiber::TERM || m_schedulFiber->getState() == Fiber::EXCEPT || m_schedulFiber->getState() == Fiber::INIT)
		&& m_threadCount == 0) {
		LOG_INFO(g_logger) << this << " stopped";
		m_stopping = true;

		if (stopping()) return;
	}

	//use_caller,使用主线程时,只在主线程中stop
	if (m_rootThread != -1) //m_scheduleFiber
	{
		DO_ASSERT(GetThis() == this);
	}
	else
	{
		//不使用主线程,任意线程都可以stop
		DO_ASSERT(GetThis() != this);
	}

	m_stopping = true;

	//唤醒线程池中的线程
	for (size_t i = 0; i < m_threadCount; ++i) {
		tickle();
	}

	//唤醒调度协程
	if (m_schedulFiber) tickle();

	//调度协程 != null 有m_scheduleFiber 一定是在主线程
	if (m_schedulFiber) {
		if (!stopping()) m_schedulFiber->call();
	}

	std::vector<Thread::ptr> thrs;
	{
		MutexType::Lock lock(m_mutex);
		thrs.swap(m_threads);
	}

	//让其他线程先结束,留一个线程来回收
	//如果在主线程stop,可能主线程先结束,然后sc就释放,但是子线程还在跑,使用sc,会core dump
	for (auto& i : thrs) {
		i->join();
	}
}

4. 调度器执行流程

  • 协程调度器初始化。协程调度器在初始化时支持传入线程数和一个布尔型的use_caller参数,表示是否使用主线程作为调度线程。在使用主线程的情况下,线程数自动减一,并且调度器内部会初始化一个属于主线程的调度协程并保存起来(比如,在main函数中创建的调度器,如果use_caller为true,那调度器会初始化一个属于main函数线程的调度协程)。
  • 调度器创建好后,即可调用调度器的schedule方法向调度器添加调度任务,但此时调度器并不会立刻执行这些任务,而是将它们保存到内部的一个任务协程队列中。
  • 调用start方法启动调度。start方法调用后会创建调度线程池,线程数量由初始化时的线程数和use_caller确定。调度线程一旦创建,就会立刻从任务队列里取任务执行。比较特殊的一点是,如果初始化时指定线程数为1且use_caller为true,那么start方法什么也不做,因为不需要创建新线程用于调度。并且,由于没有创建新的调度线程,那只能由主线程的调度协程来负责调度协程,而主线程的调度协程的执行时机与start方法并不在同一个地方,它只在stop中执行。
  • 进入调度协程的run方法。调度协程负责从调度器的协程任务队列中取任务执行。取出的任务即子协程,这里调度协程和子协程的切换模型即为前一章介绍的非对称模型,每个子协程执行完后都必须返回调度协程,由调度协程重新从协程任务队列中取新的任务并执行。如果任务队列空了,那么调度协程会切换到一个idle协程,这个idle协程什么也不做,等有新任务进来时,idle协程才会退出并回到调度协程,重新开始下一轮调度。
    • 这里需要注意:在非主线程里,调度协程就是调度线程的主协程,但在主线程里,调度协程并不是主线程的主协程,而是相当于主线程的子协程。
  • 在执行调度任务时,还可以通过调度器的GetThis()方法获取到当前调度器,再通过schedule方法继续添加新的任务,这就变相实现了在子协程中创建并运行新的子协程的功能。
  • 调度器停止。调度器的停止行为要分两种情况讨论,首先是use_caller为false的情况,这种情况下,由于没有使用caller线程进行调度,那么只需要简单地等各个调度线程的调度协程退出就行了。如果use_caller为true,表示caller线程也要参于调度,这时,调度器初始化时记录的属于caller线程的调度协程就要起作用了,在调度器停止前,应该让这个caller线程的调度协程也运行一次,让caller线程完成调度工作后再退出。如果调度器只使用了caller线程进行调度,那么所有的调度任务要在调度器停止时才会被调度。

5. 调度协程切换问题

  • 情况1:线程数为1,use_caller为false,应额外创建一个线程进行协程调度、main函数线程不参与调度的情况。
    • 因为有单独的线程用于协程调度,那只需要让新线程的入口函数作为调度协程,从协程任务队列里取任务执行就行了,main函数与调度协程完全不相关,main函数只需要向调度器添加任务,然后在适当的时机停止调度器即可。当调度器停止时,main函数要等待调度线程结束后再退出。
      在这里插入图片描述
  • 情况2:线程数为1,且use_caller为true,对应只使用main函数线程进行协程调度的情况。
    • 当只有main函数线程调度任务时,会存在以下三类协程:
      • main函数对应的主协程
      • 调度协程
      • 待调度的任务协程
    • 这三类协程运行的顺序如下:
      • main函数主协程运行,创建调度器
      • 仍然是main函数主协程运行,向调度器添加一些调度任务
      • 开始协程调度,main函数主协程让出执行权,切换到调度协程,调度协程从任务队列里按顺序执行所有的任务
      • 每次执行一个任务,调度协程都要让出执行权,再切到该任务的协程里去执行,任务执行结束后,还要再切回调度协程,继续下一个任务的调度
      • 所有任务都执行完后,调度协程还要让出执行权并切回main函数主协程,以保证程序能顺利结束。
        在这里插入图片描述
    • 抽象以下,就可以得到以下协程模型
      在这里插入图片描述
      非对称协程里,子协程只能和线程主协程切换,而不能和另一个子协程切换。在上面的情况1中,线程主协程是main函数对应的协程,另外的两类协程,也就是调度协程和任务协程,都是子协程,也就是说,调度协程不能直接和任务协程切换,一旦切换,程序的main函数协程就跑飞了。
    • 这里程序跑飞的关键是,线程只有两个线程局部变量保存主协程和子协程的上下文信息。也就是说线程任何时候都最多只能知道两个协程的上下文。如果子协程和子协程切换,那这两个上下文都会变成子协程的上下文,线程主协程的上下文丢失了,程序也就跑飞了。
    • 因此,需要给每个线程增加一个线程局部变量用于保存调度协程的上下文就可以了,这样,每个线程可以同时保存三个协程的上下文,一个是当前正在执行的协程上下文,另一个是线程主协程的上下文,最后一个是调度协程的上下文。有了这三个上下文,协程就可以根据自己的身份来选择和每次和哪个协程进行交换。
    call/back:		专门负责调度协程和主协程间转换
    swapIn/swapOut:专门负责调度协程和任务协程间转换
             call          swapIn
    主协程<--->调度协程<--->任务协程
        back      swapOut
    

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/6274.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

倒计时组件:可视化如何自定义目标时间 / 数字倒数

倒计时组件支持通过自定义目标时间或倒数数字&#xff0c;在报表和大屏中展示时间倒数和数字倒数。 下面以Sugar BI为例&#xff0c;为大家展示 倒计时展示模式 倒计时组件提供「时间倒数」和「数字倒数」两种展示模式&#xff0c;效果如下&#xff1a; 默认为「时间倒数」模…

将本地项目上传到远程仓库的步骤

文章目录将本地项目上传到远程仓库的步骤1.进入想上传的项目文件夹2.初始化本地仓库3.添加该项目下的所有文件4.将文件添加到本地仓库中5.添加远程仓库6.将文件更新到远程仓库上7.将本地文件推送回到指定的远程仓库中将本地项目上传到远程仓库的步骤 1.进入想上传的项目文件夹…

简单介绍TensorFlow中关于tf.app.flags命令行参数解析模块

这篇文章主要介绍了TensorFlow中关于tf.app.flags命令行参数解析模块&#xff0c;具有很好的参考价值&#xff0c;希望对大家有所帮助。如有错误或未考虑完全的地方&#xff0c;望不吝赐教 tf.app.flags命令行参数解析模块 说道命令行参数解析&#xff0c;就不得不提到 python…

Spring的IOC和DI入门

1、相关概念 1.1、Spring来源 官网&#xff1a;Spring | Home Spring是一个分层的Java SE/EE应用一站式的轻量级开源框架。Spring核心是IOC和AOP。 Spring主要优点包括&#xff1a; 方便解耦&#xff0c;简化开发&#xff0c;通过Spring提供的IoC容器&#xff0c;我们可以将…

分布式事务问题

分布式事务问题 1、什么是分布式事务 一次课程发布操作需要向数据库、redis、elasticsearch、MinIO写四份数据&#xff0c;这里存在分布式事务问题。 什么是分布式事务&#xff1f; 首先理解什么是本地事务&#xff1f; 平常我们在程序中通过spring去控制事务是利用数据库…

【C++】一维数组练习案例 - 五只小猪称体重

目录 1、缘起 2、案例描述 3、代码 4、总结 1、缘起 最近在黑马程序员 UP 主那里学习 C 编程语言&#xff0c;学习到了【第44节】一维数组练习案例 - 五只小猪称体重 知识点。找出五只小猪中最重的小猪&#xff0c;这不就是基本算法中的 "求最大值算法" 嘛。 为…

ChatGPT相关核心算法

ChatGPT 的卓越表现得益于其背后多项核心算法的支持和配合。本文将分别介绍作为其实现基础的 Transformer 模型、激发出其所蕴含知识的Prompt/Instruction Tuning 算法、其涌现出的思维链能力、以及确保其与人类意图对齐的基于人类反馈的强化学习算法。 1.基于Transformer的预…

STM32F4_时钟系统精讲

目录 1. 什么是系统时钟 2. 时钟树 2.1 LSI 2.2 LSE 2.3 HSI 2.4 HSE 2.5 PLLCLK 2.6 SYSCLK 2.7 HCLK 2.8 PCLK1 2.9 PCLK2 2.10 RTC/AWU 3 SysTick定时器 3.1 为什么会有Systick定时器&#xff1f; 3.2 SysTick定时器的作用 3.3 SysTick定时器的寄存器 4.…

DAMA-CDGA/CDGP数据治理认证考试地点一般有哪些?

据目前数据统计&#xff0c;进行过DAMA-CDGA/CDGP数据治理认证考试的地区主要是一线城市及直辖市&#xff0c;主要有&#xff1a;北京、上海、广州、深圳、西安、杭州、成都、重庆、武汉、厦门、太原、甘肃、长沙等。&#xff08;*当所在城市报名人数达到25人以上方可增加考场开…

C++基本语法

C 程序可以定义为对象的集合&#xff0c;这些对象通过调用彼此的方法进行交互。现在让我们简要地看一下什么是类、对象&#xff0c;方法、即时变量。 对象 - 对象具有状态和行为。例如&#xff1a;一只狗的状态 - 颜色、名称、品种&#xff0c;行为 - 摇动、叫唤、吃。对象是类…

SpringBoot2基础入门 --- 了解自动配置原理

一、SpringBoot特点 1.1、依赖管理 父项目做依赖管理 依赖管理 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.4.RELEASE</version> </parent>…

固定资产AFAB进行折旧选择页面的各个选项分别是什么意思?

固定资产AFAB进行折旧选择页面的“记账运行原因”的4个选项&#xff0c;及下面的各个选项分别是什么意思&#xff1f; 一、“记账运行原因”的4个选项&#xff1a; 1&#xff0c;计划内记账运行&#xff08;Planned posting run&#xff09;&#xff1a;上期折旧成功运行&…

测试---

1.加载测试专用属性 1.在启动测试环境时可以通过properties参数设置测试环境专用的属性 SpringBootTest(properties {"test.valuekllda"}) public class PropertiesTest {Value("${test.value}")private String msg; ​Testpublic void testPro(){System…

初识MQ——springCloud

目录 同步通讯 同步调用存在的问题 优点 异步通讯 优势 缺点 MQ常见框架 同步通讯 同步调用存在的问题 1、耦合度高 2、性能下降 3、资源浪费 4、级联失败 优点 时效性强 &#xff0c;可以立即得到结果 异步通讯 异步调用常见实现就是事件驱动模式 优势 1、解除耦合 2、…

【关于Linux中----多线程(一)】

文章目录认识线程创建线程线程优点和缺点创建一批线程终止线程线程的等待问题认识线程 在一个程序里的一个执行路线就叫做线程&#xff08;thread&#xff09;。更准确的定义是&#xff1a;线程是“一个进程内部的控制序列”一切进程至少都有一个执行线程线程在进程内部运行&a…

高通开发系列 - linux kernel内核升级msm-4.9升级至msm-4.19(1)

By: fulinux E-mail: fulinux@sina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 目录 报错和警告问题中断警告的解决方案Unknown SOC ID问题解决方法msm-vidc panic错误系统时钟功能RPM功能调试共享内存smem调试之前移植过…

SpringCloud Alibaba Nacos

文章目录第一章 SpringCloud Alibaba1.1概述1.2 主要功能1.3 组件第二章 SpringCloud Alibaba Nacos服务注册与配置中心2.1 Nacos简介2.2 安装Nacos2.3 Nacos作为服务注册中心一、服务提供者二、服务消费者三、服务注册中心对比2.4 Nacos作为服务配置中心一、Nacos基础配置项目…

【C++笔试强训】第六天

选择题 1. 解析&#xff1a;十进制转换为八进制就是不断的除8&#xff0c;取余数。十进制转换成其他进制的数就是除以进制&#xff0c;取余。 解析&#xff1a;注意printf的转换&#xff0c;%%只会打印一个%&#xff0c;所以选A。 解析&#xff1a;由于()的原因p先和*结合&…

某面试官分享经验:看求职者第一眼,开口说第一句话,面试结果就差不多定了,准确率高达90%以上...

我们以前分享过许多经验&#xff0c;但大多是站在打工人的视角上&#xff0c;今天给大家带来一个面试官的经验&#xff1a;1. 看求职者第一眼&#xff0c;开口说第一句话&#xff0c;面试结果就差不多定了&#xff0c;准确率高达90%以上。2. 绝不考八股文&#xff0c;如果问技术…

docker安装MongoBD(超详细)

一、安装docker 推荐文章&#xff1a;https://blog.csdn.net/Sumuxi9797926/article/details/127313307?spm1001.2014.3001.5502 二、创建主机挂载配置目录 data目录存放mongodb数据库文件&#xff0c;删除重启容器不会丢失 mkdir -p /docker/mongodb/data && cd …