基于C++11实现的手写线程池

news2024/11/16 11:55:29

在实际的项目中,使用线程池是非常广泛的,所以最近学习了线程池的开发,在此做一个总结。
源码:https://github.com/Cheeron955/Handwriting-threadpool-based-on-C-17

项目介绍

项目分为两个部分,在初版的时候,使用了C++11中的知识,自己实现了Any类,Semaphore类以及Result类的开发,代码比较绕,但是有很多细节是值得学习的;最终版使用了C++17提供的future类,使得代码轻量化。接下来先看初版:

test.cpp

先从test.cpp开始剖析项目构造:

#include <iostream>
#include <chrono>
#include <thread>

#include "threadpool.h"

using ULong = unsigned long long;

class MyTask : public Task
{
public:
	MyTask(int begin, int end)
		:begin_(begin)
		,end_(end)
	{
		
	}

	Any run() 
	{
		std::cout << "tid:" << std::this_thread::get_id() << "begin!" << std::endl;
		std::this_thread::sleep_for(std::chrono::seconds(3));
		ULong sum = 0;
		for (ULong i = begin_; i < end_; i++)
		{
			sum += i;
		}

		std::cout << "tid:" << std::this_thread::get_id() << "end!" << std::endl;
		return sum;
	}
private:
	int begin_;
	int end_;
};
int main()
{
	{
		ThreadPool pool;
		pool.setMode(PoolMode::MODE_CACHED);
		pool.start(4);

		Result res1 = pool.submitTask(std::make_shared<MyTask>(1, 1000000));
		Result res2 = pool.submitTask(std::make_shared<MyTask>(1, 1000000));
		pool.submitTask(std::make_shared<MyTask>(1, 1000000));
		pool.submitTask(std::make_shared<MyTask>(1, 1000000));
		pool.submitTask(std::make_shared<MyTask>(1, 1000000));
		pool.submitTask(std::make_shared<MyTask>(1, 1000000));
		ULong sum1 = res1.get().cast_<ULong>();
		std::cout << sum1 << std::endl;
	}
	
	std::cout << "main over!" << std::endl;
	getchar();

}
  • 在main函数中,创建了一个ThreadPool对象,进入ThreadPool中:

ThreadPool

重要成员变量

std::unordered_map<int, std::unique_ptr<Thread>> threads_;

//初始的线程数量 
int initThreadSize_;

//记录当前线程池里面线程的总数量
std::atomic_int curThreadSize_;

//线程数量上限阈值
int threadSizeThresHold_;

//记录空闲线程的数量
std::atomic_int idleThreadSize_;

//任务队列
std::queue<std::shared_ptr<Task>> taskQue_;

//任务数量 需要保证线程安全
std::atomic_int taskSize_;

//任务队列数量上限阈值
int taskQueMaxThresHold_;

//任务队列互斥锁,保证任务队列的线程安全
std::mutex taskQueMtx_;

//表示任务队列不满
std::condition_variable notFull_;

//表示任务队列不空
std::condition_variable notEmpty_;

//等待线程资源全部回收
std::condition_variable exitCond_;

//当前线程池的工作模式
PoolMode poolMode_;

//表示当前线程池的启动状态
std::atomic_bool isPoolRuning_;

const int TASK_MAX_THRESHHOLD = INT32_MAX;
const int THREAD_MAX_THRESHHOLD = 100;
const int THREAD_MAX_IDLE_TIME = 60; //60s
  • 具体含义请看代码中注释

重要成员函数

  1. 构造函数
//线程池构造
ThreadPool::ThreadPool()
	: initThreadSize_(4)
	, taskSize_(0)
	, idleThreadSize_(0)
	, curThreadSize_(0)
	, threadSizeThresHold_(THREAD_MAX_THRESHHOLD)
	, taskQueMaxThresHold_(TASK_MAX_THRESHHOLD)
	, poolMode_(PoolMode::MODE_FIXED)
	, isPoolRuning_(false)
{
}
  • 进行了一系列的初始化,包括线程数量,阙值等等
  1. 析构函数
ThreadPool::~ThreadPool()
{
	isPoolRuning_ = false;
	//notEmpty_.notify_all();//把等待的叫醒 进入阻塞 会死锁

	std::unique_lock<std::mutex> lock(taskQueMtx_);
	//等待线程池里面所有的线程返回用户调用ThreadPool退出 两种状态:阻塞 正在执行任务中
	notEmpty_.notify_all();//把等待的叫醒 进入阻塞
	exitCond_.wait(lock, [&]()->bool {return threads_.size() == 0; });
}
  • 析构函数中,主要是回收线程池的资源,但是这里要注意notEmpty_.notify_all();位置,如果在获得锁之前就唤醒,可能会发生死锁问题,这个在下面还会在提到。
  1. 设置线程池的工作模式
void ThreadPool::setMode(PoolMode mode)
{
	if (checkRunningState()) return;
	poolMode_ = mode;
}
  1. 设置task任务队列上限阈值
void ThreadPool::setTaskQueMaxThreshHold(int threshhold)
{
	if (checkRunningState()) return;
	taskQueMaxThresHold_ = threshhold;
}
  1. 设置线程池的工作模式,支持fixed以及cached模式
enum class PoolMode
{
	MODE_FIXED, //固定数量的线程
	MODE_CACHED, //线程数量可动态增长
};

void ThreadPool::setMode(PoolMode mode)
{
	if (checkRunningState()) return;
	poolMode_ = mode;
}
  1. 设置task任务队列上限阈值
void ThreadPool::setTaskQueMaxThreshHold(int threshhold)
{
	if (checkRunningState()) return;
	taskQueMaxThresHold_ = threshhold;
}
  1. 设置线程池cached模式下线程阈值
void ThreadPool::setThreadSizeThreshHold(int threshhold)
{
	if (checkRunningState()) return;

	if (poolMode_ == PoolMode::MODE_CACHED)
	{
		threadSizeThresHold_ = threshhold;
	}
}
  1. 给线程池提交任务,这是重中之重,用来生产任务
Result ThreadPool::submitTask(std::shared_ptr<Task> sp)
{
	//获取锁
	std::unique_lock<std::mutex> lock(taskQueMtx_);

	//线程通信 等待任务队列有空余 并且用户提交任务最长不能阻塞超过1s 否则判断提交失败,返回
	if(!notFull_.wait_for(lock, std::chrono::seconds(1),
		[&]()->bool {return taskQue_.size() < (size_t)taskQueMaxThresHold_; }))
	{ 
		
		std::cerr << "task queue is full,submit task fail." << std::endl;
		return Result(sp, false);
	}

	//如果有空余,把任务放入任务队列中
	taskQue_.emplace(sp);
	taskSize_++;

	notEmpty_.notify_all();

	if (poolMode_ == PoolMode::MODE_CACHED 
		&& taskSize_>idleThreadSize_ 
		&& curThreadSize_ < threadSizeThresHold_)
	{

		std::cout << ">>> create new thread" << std::endl;

		//创建thread线程对象
		auto ptr = std::make_unique<Thread>(std::bind(&ThreadPool::threadFunc, this, std::placeholders::_1));
		//threads_.emplace_back(std::move(ptr)); //资源转移
		int threadId = ptr->getId();
		threads_.emplace(threadId, std::move(ptr));
		threads_[threadId]->start(); //启动线程

		//修改线程个数相关的变量
		curThreadSize_++;
		idleThreadSize_++;
	}

	//返回任务的Result对象
	return Result(sp);
}
  • 在submitTask函数中,首先这是生产任务的函数,所以我们要保证线程安全,获取锁;
  • 考虑到了如果有耗时严重的任务一直占用,线程,导致提交任务一直失败,所以等待1s提交失败以后会通知用户;
  • 此时队列里面的任务没有超过阙值,就把任务放入任务队列中,更新任务数;
  • 因为新放了任务,任务队列不空了,在notEmpty_上进行通知,赶快分配线程执行任务;
  • cached模式下,需要根据任务数量和空闲线程的数量,判断是否需要创建新的线程出来,如果任务数大于现有的空闲线程数并且没有超过阙值,就增加线程,修改相关数量;
  • 返回任务的Result对象
  1. 开启线程池
void ThreadPool::start(int initThreadSize)
{
	//设置线程池的运行状态
	isPoolRuning_ = true;

	//记录初始线程个数
	initThreadSize_ = initThreadSize;
	curThreadSize_ = initThreadSize;

	//创建线程对象
	for (int i = 0; i < initThreadSize_; i++)
	{
		auto ptr = std::make_unique<Thread>(std::bind(&ThreadPool::threadFunc, this,std::placeholders::_1));
		
		int threadId = ptr->getId();
		threads_.emplace(threadId, std::move(ptr));
	}
	 
	//启动所有线程 std::vector<Thread*> threads_;
	for (int i = 0; i < initThreadSize_; i++)
	{
		threads_[i]->start(); //需要执行一个线程函数

		//记录初始空闲线程的数量
		idleThreadSize_++;
	}
}
  • 设置线程池的运行状态,如果线程在运行状态了,之前所有的设置相关的函数都不能运行了,记录初始相关数量
  • 创建线程对象,把线程函数threadFunc给到thread线程对象,使用绑定器,获取线程id,方便回收线程资源;
  • 加入线程列表std::unordered_map<int, std::unique_ptr<Thread>> 类型;
  • 启动所有线程,执行线程函数,threadFunc
void Thread::start()
{
	std::thread t(func_,threadId_);
	t.detach();
}
  1. 线程函数,从任务队列里面消费任务
void ThreadPool::threadFunc(int threadid) //线程函数返回,相应的线程就结束了
{
	auto lastTime = std::chrono::high_resolution_clock().now();

	for(;;)
	{
		std::shared_ptr<Task> task;
		
		{
			//获取锁
			std::unique_lock<std::mutex> lock(taskQueMtx_);

			std::cout << "tid:" << std::this_thread::get_id() 
				<< "尝试获取任务..." << std::endl;

				while ( taskQue_.size() == 0 )
				{

					if (!isPoolRuning_)
					{
						threads_.erase(threadid);
						std::cout << "threadid:" << std::this_thread::get_id()
							<< "exit!" << std::endl;

						//通知主线程线程被回收了,再次查看是否满足条件
						exitCond_.notify_all();
						return;
					}

					if (poolMode_ == PoolMode::MODE_CACHED)
					{	//超时返回std::cv_status::timeout
						if (std::cv_status::timeout ==
							notEmpty_.wait_for(lock, std::chrono::seconds(1)))
						{
							auto now = std::chrono::high_resolution_clock().now();
							auto dur = std::chrono::duration_cast<std::chrono::seconds>(now - lastTime);
							if (dur.count() >= THREAD_MAX_IDLE_TIME
								&& curThreadSize_ > initThreadSize_)
							{

								threads_.erase(threadid);
								curThreadSize_--;
								idleThreadSize_--;

								std::cout << "threadid:" << std::this_thread::get_id()
									<< "exit!" << std::endl;

								return;
							}
						}
					}
					else
					{
						//等待notEmpty_条件
						notEmpty_.wait(lock);
					}

					/*if (!isPoolRuning_)
					{
						threads_.erase(threadid);
						std::cout << "threadid:" << std::this_thread::get_id()
							<< "exit!" << std::endl;

						exitCond_.notify_all();
						return;
					}*/
				}
			
			idleThreadSize_--;

			std::cout << "tid:" << std::this_thread::get_id()
				<< "获取任务成功..." << std::endl;

			//从任务队列中取一个任务出来
			task = taskQue_.front();
			taskQue_.pop();
			taskSize_--;	

			//若依然有剩余任务,继续通知其他线程执行任务
			if (taskQue_.size() > 0)
			{
				notEmpty_.notify_all();
			}

			notFull_.notify_all();

		}//释放锁,使其他线程获取任务或者提交任务

		if (task != nullptr)
		{
			task->exec();
		}

		
		idleThreadSize_++;
		
		auto lastTime = std::chrono::high_resolution_clock().now();
	}
}
  • 获取任务开始的时间,便于在cached模式下,判断是否需要回收线程
  • 创造一个Task类,获取锁
class Task
{
public:

	Task();
	~Task()=default;

	void exec();

	void setResult(Result*res);

	//用户可以自定义任意任务类型,从Task继承,重写run方法,实现自定义任务处理
	virtual Any run() = 0;
private:
	Result* result_; //Result的生命周期》Task的
};
  • 如果此时任务队列里没有任务,并且主函数退出了,此时会在ThreadPool析构中设置isPoolRuning_为false,这时候就该回收线程资源了,并通知析构函数是否满足条件;
  • 如果isPoolRuning_为ture,但是在cached模式下,根据当前时间和上一次线程使用时间,判断有没有超过60s,如果超过了,并且当前线程数大于初始定义,说明不需要那么多线程了就需要回收线程资源;
  • 如果不在cached模式,就阻塞等待任务队列里面有任务
  • 获取成功任务,取出,如果队列里面还有任务,继续通知。并且取完任务,消费了一个任务 进行通知可以继续提交生产任务了,释放锁,使其他线程获取任务或者提交任务;
  • 执行任务,把任务的返回值通过setVal方法给到Result;
  • 线程处理完了,更新线程执行完任务调度的时间
  1. 检查线程池状态
bool ThreadPool::checkRunningState() const
{
	return isPoolRuning_;
}
  1. 执行任务
void Task::exec()
{
	if (result_ != nullptr)
	{
		result_->setVal(run()); //多态调用,run是用户的任务
	}
}

void Task::setResult(Result* res)
{
	result_ = res;
}
  • 把任务的返回值通过setVal方法给到Result
  1. 信号量类
class Semaphore
{
public:
	Semaphore(int limit = 0)
		:resLimit_(limit)
	{}

	~Semaphore() = default;

	void wait()
	{  
		std::unique_lock<std::mutex> lock(mtx_);
		//等待信号量有资源 没有资源的话 会阻塞当前线程
		cond_.wait(lock, [&]()->bool { return resLimit_ > 0; });
		resLimit_--;
	}

	void post()
	{
		std::unique_lock<std::mutex> lock(mtx_);
		resLimit_++;

		cond_.notify_all();
	}
private:

	int resLimit_;
	std::mutex mtx_;
	std::condition_variable cond_;

};
  • 在信号量类中使用了条件变量和互斥锁实现了信号量的实现,等待信号量资源和释放信号量资源。
  1. Any类
class Any
{
public:
	Any() = default;
	~Any() = default;

	//左值
	Any(const Any&) = delete;
	Any& operator=(const Any&) = delete;

	//右值
	Any(Any&&) = default;
	Any& operator=(Any&&) = default;

	template<typename T>


	Any(T data) :base_(std::make_unique<Derive<T>>(data))
	{}


	template<typename T>
	T cast_()
	{
		Derive<T> *pd = dynamic_cast<Derive<T>*>(base_.get());
		if (pd == nullptr)
		{
			throw "type is unmatch";
		}
		return pd->data_;
	}

private:
	//基类类型
	class Base
	{
	public:
		virtual ~Base() = default;
	};

	//派生类类型
	template<typename T>//模板
	class Derive :public Base
	{
	public:
		Derive(T data) : data_(data)
		{}
		T data_; //保存了任意的其他类型
	};

private:
	//定义一个基类指针,基类指针可以指向派生类对象
	std::unique_ptr<Base> base_;
};
  • 定义了一个基类Base
  • 定义了一个模板类的派生类类型,继承Base,其中保存了任意的其他类型;
  • 对象包在派生类对象里面,通过基类指针指向派生类对象,构造函数可以让Any类型接收任意其他的数据类型,用户就可以使用任意期望的类型;
  • cast_()方法把Any对象里面存储的data数据提取出来,基类指针指向 派生类指针 ,使用强转dynamic_cast将基类指针或引用转换为派生类指针或引用,获取了指向的Derive对象,然后提取出来data_;
  1. Result方法的实现
class Result
{
public:

	Result(std::shared_ptr<Task> task, bool isValid = true);
	~Result() = default;

	//setVal方法,获取任务执行完的返回值
	void setVal(Any any);

	//用户调用get方法,获取task的返回值
	Any get();
private:
	//存储任务的返回值
	Any any_;

	//线程通信信号量
	Semaphore sem_;

	//指向对应获取返回值的任务对象
	std::shared_ptr<Task> task_;

	//返回值是否有效
	std::atomic_bool isValid_;
};


Result::Result(std::shared_ptr<Task> task, bool isValid)
		:isValid_(isValid)
		,task_(task)
{
	task_->setResult(this);
}

Any Result::get()
{
	if (!isValid_)
	{
		return " ";
	}

	//task任务如果没有执行完,这里会阻塞用户的线程
	sem_.wait();
	return std::move(any_);
}

void Result::setVal(Any any)
{
	//存储task的返回值
	this->any_ = std::move(any);

	//已经获取了任务的返回值,增加信号量资源
	sem_.post();
}
  • Result 实现接受提交到线程池的task任务执行完成后的返回值类型result;
  • 设置了setVal方法,获取任务执行完的返回值和用户调用get方法,获取task的返回值,使用了信号量等到setVal设置成功,才能获取值,否则会进入阻塞;

回到test.cpp

  • 定义了一个ThreadPool对象,默认是固定的,可以修改为cached模式,然后开启线程(可以使用hardware_concurrency()获取cpu核心数量);
  • 提交任务submitTask;
  • 出 } 进行析构

举个栗子~

在cached模式,代码如上test.cpp
在这里插入图片描述
可以看到,目前四个线程,六个任务,所以创建了两个线程;
六个线程获取任务成功,然后释放资源成功;

固定线程:

int main()
{
	{
		ThreadPool pool;
		pool.start(4);

		Result res1 = pool.submitTask(std::make_shared<MyTask>(1, 1000000));
		Result res2 = pool.submitTask(std::make_shared<MyTask>(1, 1000000));
		pool.submitTask(std::make_shared<MyTask>(1, 1000000));
		pool.submitTask(std::make_shared<MyTask>(1, 1000000));
		pool.submitTask(std::make_shared<MyTask>(1, 1000000));

		ULong sum1 = res1.get().cast_<ULong>();
		std::cout << sum1 << std::endl;
	}
	
	std::cout << "main over!" << std::endl;
	getchar();
}

在这里插入图片描述
有四个线程,五个任务,11676线程获取了两次任务,最后回收线程资源。

好了~基于C++11实现的手写线程池,就到此结束了。除此之外,在GitHub上,提供了linux下的使用方法,感兴趣的小伙伴可以按照步骤实现一下 ~ 注意死锁问题!下一节会剖析基于C++17实现的手写线程池,代码会看起来很轻便,下一节见 ~

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于springboot+vue的社区医院管理服务系统

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

Numba 的 CUDA 示例(1/4):踏上并行之旅

按照本系列从头开始使用 Python 学习 CUDA 编程 介绍 GPU&#xff08;图形处理单元&#xff09;&#xff0c;顾名思义&#xff0c;最初是为计算机图形学开发的。从那时起&#xff0c;它们几乎在每个需要高计算吞吐量的领域都无处不在。这一进步得益于 GPGPU&#xff08;通用 G…

AI大模型如何“开窍”?算法、数据与架构的三重奏

一、算法创新 1. 探索新的学习范式 自监督学习&#xff1a;利用未标注数据让模型自我学习&#xff0c;提高模型的泛化能力。元学习&#xff1a;让模型学会如何学习&#xff0c;以便在不同任务之间快速迁移。强化学习&#xff1a;通过试错与奖励机制&#xff0c;使模型在与环境…

[数据集][目标检测]红外车辆检测数据集VOC+YOLO格式13979张类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;13979 标注数量(xml文件个数)&#xff1a;13979 标注数量(txt文件个数)&#xff1a;13979 标…

介绍Django Ninja框架

文章目录 安装快速开始特性详解自动文档生成定义请求和响应模型异步支持中间件支持测试客户端 结论 Django Ninja是一个基于Python的快速API开发框架&#xff0c;它结合了Django和FastAPI的优点&#xff0c;提供了简单易用的方式来构建高性能的Web API。 安装 使用以下命令安…

[FlareOn6]Overlong

很简单的逻辑 一度让我以为是加保护了 运行告诉我从未编码,懵逼 动调你也发现,你根本没什么可以操作的空间,密文什么的,都是固定的 但是这里大家发现没 我们只加密了28个密文 然后text是128 也就是 0x80 是不是因为密文没加密完呢 我也懒得去写代码了 汇编直接修改push 字…

没有可用软件包 docker-ce。 错误:无须任何处理

特么的各种百度查看&#xff0c;全是一些废话&#xff01;&#xff01;&#xff01;centos7安装不上docker&#xff0c;都是老的代码了&#xff1a; yum install docker-ce 解决方案&#xff1a; # CentOS yum install docker-io

Android14 WMS-窗口添加流程(一)-Client端

窗口布局在onCreate方法中通过setContentView(R.layout.xxx)加载&#xff0c;但窗口的显示并不是在wm_on_create_called中, 而是在wm_on_resume_called后&#xff0c;也就是说应用onResume时此窗口是不可见的&#xff0c;真正可见是当此window窗口的mDrawState变化状态从NO_SUR…

JVS物联网、逻辑引擎、智能BI(重构优化)5.21功能新增说明

项目介绍 JVS是企业级数字化服务构建的基础脚手架&#xff0c;主要解决企业信息化项目交付难、实施效率低、开发成本高的问题&#xff0c;采用微服务配置化的方式&#xff0c;提供了 低代码数据分析物联网的核心能力产品&#xff0c;并构建了协同办公、企业常用的管理工具等&am…

AI播客下载:Acquired podcast每个公司都有一个故事

"Acquired Podcast" 是一档专注于深度解析科技行业和企业发展历程的播客节目&#xff0c;由Ben Gilbert和David Rosenthal主持。其口号是&#xff1a;Every company has a story.《Acquired》每一集都围绕一个特定的主题或公司进行讨论。它以独特的视角和深入的分析&…

数字孪生技术助力智慧园区建设

随着城市化进程的加速和科技创新的推动&#xff0c;城市面临着诸多挑战和机遇。如何提升城市的竞争力和可持续性&#xff0c;是一个亟待解决的问题。在这个背景下&#xff0c;智慧园区作为一种新型的城市发展模式&#xff0c;引起了越来越多的关注和探索。 什么是智慧园区&…

科技查新是什么?一文了解!

本文主要解答 1、什么是科技查新&#xff1f; 2、科技查新有哪些作用&#xff1f; 3、科技查新一般应用于什么地方&#xff1f; 4、在哪能出具正规查新报告&#xff1f; 5、科技查新流程是怎样的&#xff1f; 带着这些问题阅读这篇文章相信一定会有收获&#xff01;干活内…

Jetson Orin Nano v6.0 + tensorflow2.15.0+nv24.05 GPU版本安装

Jetson Orin Nano v6.0 tensorflow2.15.0nv24.05 GPU版本安装 1. 源由2. 步骤2.1 Step1&#xff1a;系统安装2.2 Step2: nvidia-jetpack安装2.3 Step3&#xff1a;jtop安装2.4 Step4&#xff1a;h5py安装2.5 Step5&#xff1a;tensorflow安装2.6 Step6&#xff1a;jupyterlab安…

Axios的使用简单说明

axios 请求方式和参数 axios 可以发送 ajax 请求&#xff0c;不同的方法可以发送不同的请求: axios.get&#xff1a;发送get请求 axios.post&#xff1a;发送post请求 axios.put&#xff1a;发送put请求 axios.delete&#xff1a;发送delete请求 无论哪种方法&#xff0c;第一…

计算机考研|408开始的晚,如何规划复习?

408开始的比较晚不用怕 只需要6个月的复习时间&#xff0c;按照我的复习计划&#xff0c;你至少可以考100这个分数 我的计划是对于基础差的考生的&#xff0c;大佬们就可以绕道了&#xff0c;对于基础差的考生来说&#xff0c;408复习的难点其实在于学好数据结构和计算机组成…

如何使用OCR批量提取短剧和电视剧内的字幕?

为什么使用光字符识别 (OCR) 提取视频字幕&#xff1f; 视频字幕提取是将视频中的字幕转换为 SRT 格式文本的过程。传统方法是使用自动语音识别 (ASR) 技术&#xff0c;但对于某些类型的视频&#xff08;例如短剧、电视剧、电影、访谈等节目&#xff09;&#xff0c;由于这类型…

代码随想录——合并二叉树(Leetcode617)

题目链接 层序遍历 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right) …

C++青少年简明教程:for循环语句

C青少年简明教程&#xff1a;for循环语句 C的for循环语句是一种迭代控制语句&#xff0c;用于重复执行一段代码。 语法格式&#xff1a; for(表达式1&#xff1b;表达式2&#xff1b;表达式3) 循环体 for循环语句执行流程图&#xff1a; 不太好理解&#xff0c;请看下图&am…

如何在生产环境中以非 Root 用户启动 Kafka

目录 如何在生产环境中以非 Root 用户启动 Kafka1. 创建 Kafka 用户2. 设置目录权限3. 配置 systemd 服务文件4. 启动和启用 Kafka 服务5. 验证 Kafka 服务经验总结 为了在生产环境中以非 root 用户&#xff08;如 kafka 用户&#xff09;启动 Kafka&#xff0c;您需要确保 Ka…

Unity射击游戏开发教程:(28)敌人被摧毁时掉落的能量提升

在这篇文章中,我将介绍如何在敌人被摧毁时产生能量提升。 首先,有一个生成管理器,负责生成敌人和能量提升。我正在对其进行转换,以便当敌人被摧毁时,有可能会掉落能量。本文将仅介绍当敌人被摧毁时掉落的能量道具。我将介绍为电源添加一个平衡的生成系统。 Spawn Manager…