线程池分析与设计

线程池

基本功能接口

C++11 及以后的标准中,std::packaged_taskstd::future是并发编程中用于任务封装和结果获取的重要组件,它们通常与线程配合使用,实现异步操作。

std::packaged_task

std::packaged_task:封装可调用对象为异步任务,它是一个模板类,用于封装任何可调用对象(包括函数、lambda、函数对象等),并且它还需要与std::future关联使用,当std::packaged_task被执行时,其中封装的任务也会运行,结果会存储在内部,和这个std::packaged_task关联的std::future可以进行调用。

核心功能
  1. 异步任务封装:将任务(在线程池中每个任务起始就是一个函数)打包起来,让它可以异步执行
  2. std::future绑定:因为每一个std::packaged_task()会对应一个std::future,所以在std::packaged_task执行之后,其中的任务也运行了,结果就存储在内部,等待std::future通过get_future()调用.
  3. 执行任务:可以直接通过operator()调用,也可传递给线程执行(std::thread接收std::packaged_task()当参数就行)
简单函数的示例

这里先给出一个简单的小示例,之后会结合线程池进行阐述

#include <future>
#include <thread>
#include <iostream>int add(int a, int b) {return a + b;
}int main()
{// 1、首先需要封装任务-->这里是封装add(),需要一个int做返回值以及两个int参数std::packaged_task<int(int, int)> task(add);// 2、和std::future绑定   异步任务task调用get_future进行绑定std::future<int> f = task.get_future();// 3、在一个线程中执行这个封装好的异步任务std::thread t(std::move(task), 10, 20);// 这里需要强调一下,封装后的异步任务不可复制,只能进行移动,所以再传给线程做参数时,只能使用std::move()// 因为这里是异步执行的,所以主线程可以执行其他任务// 现在获取这个线程中执行的结果// future() 具有唯一性,使用get()获取一次之后,就不能获取第二次了,如果结果没有就绪就阻塞等待结果就绪int res = f.get();	// 现在就可以打印获得的这个结果了std::cout << "res = " << res << std::endl;// 在创建线程之后,必须 join或者detach,否则就会出现错误t.join();return 0;
}

那么接下来是关于std::future获取结果

std::future

用于获取异步操作(线程、任务)执行的结果,可以理解为一种类似于“未来结果的占位符”,因为你启动一个异步线程时,可能无法立即得到结果,但是可以使用std::future对象在未来某个时刻获取结果。

核心功能
  1. 可以通过get()方法获取异步操作得到的结果(返回值),如果在调用get()时,异步操作还未完成,那么就会阻塞当前线程等待有结果产生。
  2. 可以通过valid()判断future是否与一个有效的异步操作关联成功,可以通过wait()阻塞等待结果,也可以通过wait_for()wait_until()等待指定时长之后返回状态。

上方已经给出了示例用法,都是一样的,这里就不给了,待会直接上线程池相关的示例。

任务入队操作

当有新任务到来时,任务会被添加到任务队列中,这个过程中,需要先获取互斥锁,保证任务队列的线程安全,添加任务后,通过条件变量通知等待的线程有新任务到来。我这里将任务划分成了不带返回值的普通任务和带返回值的任务,其中带返回值的任务使用异步封包的方式进行封装,分别如下:

带返回值的异步任务提交到任务队

步骤:

  1. 通过std::bind()std::make_shared()创建一个包装了任务的std::package_task
  2. 获取其对应的std::future用于获取任务执行结果
  3. 在临界区内(加锁)将任务添加到任务队列tasks
  4. 通知一个等待的线程有新任务

以下是线程池提交带有返回值的任务的示例过程

template<typename F, typename... Args>
auto SubmitRetTask(F&& func, Args... args) -> std::future<std::invoke_result_t<F, Args...>> {// 首先定义返回类型auto ret_type = std::invoke_result_t<F, Args...>;// 状态判断if (is_shuntdown_.load() || !is_available_.load()) {// 返回一个控制return std::future<ret_type>();}// 开始封装异步任务auto bind_task = std::bind(std::forward<F>(func), std::forward<Args>(args...));auto task = std::make_shared<std::packaged_task<ret_type>()>(std::move(bind_task));std::future<ret_type> res = task.get_future();{// 在临界区加锁,将任务添加到任务队列中std::lock_guard<std::mutex> lock(task_mutex_);tasks_.emplace([task](){(*task)();});}task_cv_.notify_one();return res;
}// 用到的成员变量
std::queue<std::function<void()> tasks_;	// 任务队列
std::atomic<bool> is_shutdown_;			// 线程是否关闭
std::atomic<bool> is_available_;		// 线程池是否还有效std::mutex task_mutex_;					// 任务锁
std::condition_variable task_cv_;		// 条件变量,用于阻塞任务

不带返回值的普通任务

template<typename F, typename... Args>
void SubmitTask(F&& func, Args... args) {// 终止条件if (is_shutdown_.load() || !is_available_.load()) {return;}// 封装任务auto task = std::bind(std::forward<F>(func), std::forward<Args>(args...));{std::lock_guard<std::mutex> lock(task_mutex_);tasks.emplace([task](){task();		// 调用对应的任务});}// 唤醒一个阻塞中的线程task_cv_.notify_one();
}

所以可以看出,起始线程池中任务的提交过程整体思路都是一致的,只是有返回值的提交上,添加了std::packaged_taskstd::future来做异步任务的封装而已。

工作线程取出任务执行过程

在工作线程开启之后,需要去任务队列中取出任务然后执行。主要的过程是,获取互斥锁保证资源的互斥访问,然后检查任务队列是否为空,如果为空,就需要通过条件变量阻塞,等待任务添加进来。获取到任务之后就会执行任务,执行完毕马上继续获取任务,除非线程池停止并且任务队列为空。

主要的过程如下:

  1. 由于每次都会取出一个任务task,每个任务都是一个函数std::function<void()>
  2. 无限循环,一直访问任务队列,直到线程池停止,然后任务队列为空
  3. 取出任务队列中的任务,执行

我的取出任务的接口函数

成员变量信息

using ThreadPtr = std::shared_ptr<std::thread>;
using Task = std::function<void()>;// 一个线程信息结构体,包含管理线程的智能指针
struct ThreadInfo {ThreadInfo();~ThreadInfo();ThreadPtr ptr{nullptr};
}// 每一个线程的信息都是有一个智能指针来管理
using ThreadInfoPtr = std::shared_ptr<ThreadInfo>;// 线程数组
std::vector<ThreadInfoPtr> work_threads_;

添加线程函数

void ThreadPool::AddThread() {// 先从任务队列中取出一个任务auto func = [this]() {while (true) {Task task;{// 首先获取互斥锁std::unique_lock<std::mutex> lock(task_mutex_);// 通过条件变量等待条件满足task_cv_.wait(lock, [this](){return is_shutdown_.load() || !tasks.empty();});if (is_shutdown_.load() && tasks.empty()) {return;}// 取出任务task = std::move(tasks.front());tasks.pop();}task();}};// 将取出来的任务封装到线程中添加到线程池ThreadInfoPtr thread_ptr = std::shared_ptr<std::thread>();thread_ptr->ptr = std::make_shared<ThreadInfo>(std::move(func));// 添加到线程池中work_threads_.emplace_back(std::move(thread_ptr));
}

线程池类设计

线程池类负责创建线程池、销毁线程池以及管理线程队列、任务队列以及添加任务或者取出任务执行等操作。

类定义如下:

class ThreadPool{
public:explicit ThreadPool(uint32_t thread_count);// 禁止拷贝线程池ThreadPool(const ThreadPool&) = delete;ThreadPool& operator=(const ThreadPool&) = delete;~ThreadPool();bool Start();	// 启动线程池void Stop();	// 停止线程池// 提交任务,分别有普通任务和带返回值的任务template<typename F, typename... Args>void SubmitTask(F&& func, Args... args) {if (is_shutdown_.load() || !is_available_.load()) {return;}auto task = std::bind(std::forward<F>(func), std::forward<Args>(args...));{std::unique_lock<std::mutex> lock(task_mutex_);// 添加任务tasks.emplace([task](){task();});}// 唤醒一个等待任务的阻塞线程task_cv_.notify_one();}// 提交带有返回值的任务template<typename F, typename... Args>auto SubmitRetTask(F&& func, Args... args) -> std::future<std::invoke_result_t<F, Args...>> {auto ret_type = std::invoke_result_t<F, Args...>;// 检查变量判断是否还能继续if (is_shutdown_.load() || !is_available_.load()) {return std::future<ret_type>();		// 此时需要返回一个空对象}auto bind_task = std::bind(std::forward<F>(func), std::forward<Args>(args...));// 用packaged_task和shared_ptr封装异步任务auto task = std::make_shared<std::packaged_task<ret_type>()>(std::move(bind_task));// 与future绑定std::future<ret_type> res = task.get_future();{std::unique_lock<std::mutex> lock(task_mutex_);tasks_.emplace([task](){(*task)();});}// 唤醒等待线程task_cv_.notify_one();return res;}private:// 增加线程函数void AddThread();// 通过智能指针来管理线程using ThreadPtr = std::shared_ptr<std::thread>;using Task = std::function<void()>;struct ThreadInfo{ThreadInfo();~ThreadInfo();ThreadInfo ptr{nullptr};}using ThreadInfoPtr = std::shared_ptr<ThreadInfo>;std::vector<ThreadInfoPtr> works_threads_;	std::queue<Task> tasks_;std::mutex task_mutex_;std::condition_variable task_cv_;std::atomic<uint32_t> thread_count_;std::atomic<bool> is_shutdown_;std::atomic<bool> is_available_;
}

接口实现

构造与析构

我这里的思路是构造函数初始化一些基本的成员变量,比如thread_count_,is_shutdown_,is_available_就够了,在启动线程池时采取初始化,并且创建线程添加到线程池中,所以构造函数如下:

explicit ThreadPool::ThreadPool(uint32_t thread_count) : thread_count_(thread_count), is_shutdown_(false), is_available(false){}

析构函数和构造函数的思路类似,里面由Stop()这个接口来处理线程池的终止

ThreadPool::~ThreadPool() { Stop();}
Start() 启动线程池和 Stop()终止线程池

Start()负责启动线程池,然后循环创建线程并且添加到容器中

bool ThreadPool::Start() {if (!is_available_.load()) {is_availeable_.store(true);uint32_t thread_count = thread_count_.load();for (uint32_t i = 0; i < thread_count; i++) {AddThread();	// 由这个添加函数完成创建线程并且绑定任务添加到容器中}return true;}return false;
}

Stop()代表线程池停止接口,首先需要将所有相关的成员变量置为停止状态下对应的值,然后停止所有进程,回收所有进程,保证所有进程只join()一次

void ThreadPool::Stop() {if (!is_shotdown_.load()) {return ;}// 将对应的变量置为退出状态is_shutdown_.store(true);is_available_.store(false);// 通知所有线程task_cv_.notify_all();// 回收所有线程for (auto& thread_info_ptr : work_threads_) {if (thread_info_ptr && thread_info_ptr->ptr) {std::thread& t = *thread_info_ptr->ptr;if (t.joinable()) {t.join();}}}// 清空所有线程容器work_threads_.clear();{// 在线程池关闭的时候,还需要将任务队列中的所有任务popstd::lock_guard<std::mutex> lock(task_mutex_);while (!tasks_.empty()) {tasks_.pop();}}
}

取出任务绑定线程然后添加到线程函数 AddThread()

AddThread()这个函数主要是从任务队列中取出任务,然后将其绑定到线程,并且添加到容器中.

void ThreadPool::AddThread() {// 取出任务auto func = [this]() {while(true) {Task task;{std::unqiue_lock<std::mutex> lock(task_mutex_);task_cv_.wait(lock, [this](){return is_shutdown_.load() || !tasks.empty(); });if (is_shutdown_.load() && tasks.empty()) {return;}// 取出任务task = std::move(tasks.front());tasks.pop();}task();}}// 将其封装为线程ThreadInfoPtr thread_ptr = std::make_shared<ThreadInfo>();thread_ptr->ptr = std::make_shared<std::thread>(std::move(func));work_threads_.emplace_back(std::move(thread_ptr));
}

线程池

基本功能接口

C++11 及以后的标准中,std::packaged_taskstd::future是并发编程中用于任务封装和结果获取的重要组件,它们通常与线程配合使用,实现异步操作。

std::packaged_task

std::packaged_task:封装可调用对象为异步任务,它是一个模板类,用于封装任何可调用对象(包括函数、lambda、函数对象等),并且它还需要与std::future关联使用,当std::packaged_task被执行时,其中封装的任务也会运行,结果会存储在内部,和这个std::packaged_task关联的std::future可以进行调用。

核心功能
  1. 异步任务封装:将任务(在线程池中每个任务起始就是一个函数)打包起来,让它可以异步执行
  2. std::future绑定:因为每一个std::packaged_task()会对应一个std::future,所以在std::packaged_task执行之后,其中的任务也运行了,结果就存储在内部,等待std::future通过get_future()调用.
  3. 执行任务:可以直接通过operator()调用,也可传递给线程执行(std::thread接收std::packaged_task()当参数就行)
简单函数的示例

这里先给出一个简单的小示例,之后会结合线程池进行阐述

#include <future>
#include <thread>
#include <iostream>int add(int a, int b) {return a + b;
}int main()
{// 1、首先需要封装任务-->这里是封装add(),需要一个int做返回值以及两个int参数std::packaged_task<int(int, int)> task(add);// 2、和std::future绑定   异步任务task调用get_future进行绑定std::future<int> f = task.get_future();// 3、在一个线程中执行这个封装好的异步任务std::thread t(std::move(task), 10, 20);// 这里需要强调一下,封装后的异步任务不可复制,只能进行移动,所以再传给线程做参数时,只能使用std::move()// 因为这里是异步执行的,所以主线程可以执行其他任务// 现在获取这个线程中执行的结果// future() 具有唯一性,使用get()获取一次之后,就不能获取第二次了,如果结果没有就绪就阻塞等待结果就绪int res = f.get();	// 现在就可以打印获得的这个结果了std::cout << "res = " << res << std::endl;// 在创建线程之后,必须 join或者detach,否则就会出现错误t.join();return 0;
}

那么接下来是关于std::future获取结果

std::future

用于获取异步操作(线程、任务)执行的结果,可以理解为一种类似于“未来结果的占位符”,因为你启动一个异步线程时,可能无法立即得到结果,但是可以使用std::future对象在未来某个时刻获取结果。

核心功能
  1. 可以通过get()方法获取异步操作得到的结果(返回值),如果在调用get()时,异步操作还未完成,那么就会阻塞当前线程等待有结果产生。
  2. 可以通过valid()判断future是否与一个有效的异步操作关联成功,可以通过wait()阻塞等待结果,也可以通过wait_for()wait_until()等待指定时长之后返回状态。

上方已经给出了示例用法,都是一样的,这里就不给了,待会直接上线程池相关的示例。

任务入队操作

当有新任务到来时,任务会被添加到任务队列中,这个过程中,需要先获取互斥锁,保证任务队列的线程安全,添加任务后,通过条件变量通知等待的线程有新任务到来。我这里将任务划分成了不带返回值的普通任务和带返回值的任务,其中带返回值的任务使用异步封包的方式进行封装,分别如下:

带返回值的异步任务提交到任务队

步骤:

  1. 通过std::bind()std::make_shared()创建一个包装了任务的std::package_task
  2. 获取其对应的std::future用于获取任务执行结果
  3. 在临界区内(加锁)将任务添加到任务队列tasks
  4. 通知一个等待的线程有新任务

以下是线程池提交带有返回值的任务的示例过程

template<typename F, typename... Args>
auto SubmitRetTask(F&& func, Args... args) -> std::future<std::invoke_result_t<F, Args...>> {// 首先定义返回类型auto ret_type = std::invoke_result_t<F, Args...>;// 状态判断if (is_shuntdown_.load() || !is_available_.load()) {// 返回一个控制return std::future<ret_type>();}// 开始封装异步任务auto bind_task = std::bind(std::forward<F>(func), std::forward<Args>(args...));auto task = std::make_shared<std::packaged_task<ret_type>()>(std::move(bind_task));std::future<ret_type> res = task.get_future();{// 在临界区加锁,将任务添加到任务队列中std::lock_guard<std::mutex> lock(task_mutex_);tasks_.emplace([task](){(*task)();});}task_cv_.notify_one();return res;
}// 用到的成员变量
std::queue<std::function<void()> tasks_;	// 任务队列
std::atomic<bool> is_shutdown_;			// 线程是否关闭
std::atomic<bool> is_available_;		// 线程池是否还有效std::mutex task_mutex_;					// 任务锁
std::condition_variable task_cv_;		// 条件变量,用于阻塞任务

不带返回值的普通任务

template<typename F, typename... Args>
void SubmitTask(F&& func, Args... args) {// 终止条件if (is_shutdown_.load() || !is_available_.load()) {return;}// 封装任务auto task = std::bind(std::forward<F>(func), std::forward<Args>(args...));{std::lock_guard<std::mutex> lock(task_mutex_);tasks.emplace([task](){task();		// 调用对应的任务});}// 唤醒一个阻塞中的线程task_cv_.notify_one();
}

所以可以看出,起始线程池中任务的提交过程整体思路都是一致的,只是有返回值的提交上,添加了std::packaged_taskstd::future来做异步任务的封装而已。

工作线程取出任务执行过程

在工作线程开启之后,需要去任务队列中取出任务然后执行。主要的过程是,获取互斥锁保证资源的互斥访问,然后检查任务队列是否为空,如果为空,就需要通过条件变量阻塞,等待任务添加进来。获取到任务之后就会执行任务,执行完毕马上继续获取任务,除非线程池停止并且任务队列为空。

主要的过程如下:

  1. 由于每次都会取出一个任务task,每个任务都是一个函数std::function<void()>
  2. 无限循环,一直访问任务队列,直到线程池停止,然后任务队列为空
  3. 取出任务队列中的任务,执行

我的取出任务的接口函数

成员变量信息

using ThreadPtr = std::shared_ptr<std::thread>;
using Task = std::function<void()>;// 一个线程信息结构体,包含管理线程的智能指针
struct ThreadInfo {ThreadInfo();~ThreadInfo();ThreadPtr ptr{nullptr};
}// 每一个线程的信息都是有一个智能指针来管理
using ThreadInfoPtr = std::shared_ptr<ThreadInfo>;// 线程数组
std::vector<ThreadInfoPtr> work_threads_;

添加线程函数

void ThreadPool::AddThread() {// 先从任务队列中取出一个任务auto func = [this]() {while (true) {Task task;{// 首先获取互斥锁std::unique_lock<std::mutex> lock(task_mutex_);// 通过条件变量等待条件满足task_cv_.wait(lock, [this](){return is_shutdown_.load() || !tasks.empty();});if (is_shutdown_.load() && tasks.empty()) {return;}// 取出任务task = std::move(tasks.front());tasks.pop();}task();}};// 将取出来的任务封装到线程中添加到线程池ThreadInfoPtr thread_ptr = std::shared_ptr<std::thread>();thread_ptr->ptr = std::make_shared<ThreadInfo>(std::move(func));// 添加到线程池中work_threads_.emplace_back(std::move(thread_ptr));
}

线程池类设计

线程池类负责创建线程池、销毁线程池以及管理线程队列、任务队列以及添加任务或者取出任务执行等操作。

类定义如下:

class ThreadPool{
public:explicit ThreadPool(uint32_t thread_count);// 禁止拷贝线程池ThreadPool(const ThreadPool&) = delete;ThreadPool& operator=(const ThreadPool&) = delete;~ThreadPool();bool Start();	// 启动线程池void Stop();	// 停止线程池// 提交任务,分别有普通任务和带返回值的任务template<typename F, typename... Args>void SubmitTask(F&& func, Args... args) {if (is_shutdown_.load() || !is_available_.load()) {return;}auto task = std::bind(std::forward<F>(func), std::forward<Args>(args...));{std::unique_lock<std::mutex> lock(task_mutex_);// 添加任务tasks.emplace([task](){task();});}// 唤醒一个等待任务的阻塞线程task_cv_.notify_one();}// 提交带有返回值的任务template<typename F, typename... Args>auto SubmitRetTask(F&& func, Args... args) -> std::future<std::invoke_result_t<F, Args...>> {auto ret_type = std::invoke_result_t<F, Args...>;// 检查变量判断是否还能继续if (is_shutdown_.load() || !is_available_.load()) {return std::future<ret_type>();		// 此时需要返回一个空对象}auto bind_task = std::bind(std::forward<F>(func), std::forward<Args>(args...));// 用packaged_task和shared_ptr封装异步任务auto task = std::make_shared<std::packaged_task<ret_type>()>(std::move(bind_task));// 与future绑定std::future<ret_type> res = task.get_future();{std::unique_lock<std::mutex> lock(task_mutex_);tasks_.emplace([task](){(*task)();});}// 唤醒等待线程task_cv_.notify_one();return res;}private:// 增加线程函数void AddThread();// 通过智能指针来管理线程using ThreadPtr = std::shared_ptr<std::thread>;using Task = std::function<void()>;struct ThreadInfo{ThreadInfo();~ThreadInfo();ThreadInfo ptr{nullptr};}using ThreadInfoPtr = std::shared_ptr<ThreadInfo>;std::vector<ThreadInfoPtr> works_threads_;	std::queue<Task> tasks_;std::mutex task_mutex_;std::condition_variable task_cv_;std::atomic<uint32_t> thread_count_;std::atomic<bool> is_shutdown_;std::atomic<bool> is_available_;
}

接口实现

构造与析构

我这里的思路是构造函数初始化一些基本的成员变量,比如thread_count_,is_shutdown_,is_available_就够了,在启动线程池时采取初始化,并且创建线程添加到线程池中,所以构造函数如下:

explicit ThreadPool::ThreadPool(uint32_t thread_count) : thread_count_(thread_count), is_shutdown_(false), is_available(false){}

析构函数和构造函数的思路类似,里面由Stop()这个接口来处理线程池的终止

ThreadPool::~ThreadPool() { Stop();}
Start() 启动线程池和 Stop()终止线程池

Start()负责启动线程池,然后循环创建线程并且添加到容器中

bool ThreadPool::Start() {if (!is_available_.load()) {is_availeable_.store(true);uint32_t thread_count = thread_count_.load();for (uint32_t i = 0; i < thread_count; i++) {AddThread();	// 由这个添加函数完成创建线程并且绑定任务添加到容器中}return true;}return false;
}

Stop()代表线程池停止接口,首先需要将所有相关的成员变量置为停止状态下对应的值,然后停止所有进程,回收所有进程,保证所有进程只join()一次

void ThreadPool::Stop() {if (!is_shotdown_.load()) {return ;}// 将对应的变量置为退出状态is_shutdown_.store(true);is_available_.store(false);// 通知所有线程task_cv_.notify_all();// 回收所有线程for (auto& thread_info_ptr : work_threads_) {if (thread_info_ptr && thread_info_ptr->ptr) {std::thread& t = *thread_info_ptr->ptr;if (t.joinable()) {t.join();}}}// 清空所有线程容器work_threads_.clear();{// 在线程池关闭的时候,还需要将任务队列中的所有任务popstd::lock_guard<std::mutex> lock(task_mutex_);while (!tasks_.empty()) {tasks_.pop();}}
}

取出任务绑定线程然后添加到线程函数 AddThread()

AddThread()这个函数主要是从任务队列中取出任务,然后将其绑定到线程,并且添加到容器中.

void ThreadPool::AddThread() {// 取出任务auto func = [this]() {while(true) {Task task;{std::unqiue_lock<std::mutex> lock(task_mutex_);task_cv_.wait(lock, [this](){return is_shutdown_.load() || !tasks.empty(); });if (is_shutdown_.load() && tasks.empty()) {return;}// 取出任务task = std::move(tasks.front());tasks.pop();}task();}}// 将其封装为线程ThreadInfoPtr thread_ptr = std::make_shared<ThreadInfo>();thread_ptr->ptr = std::make_shared<std::thread>(std::move(func));work_threads_.emplace_back(std::move(thread_ptr));
}

最后,希望自己继续加油,学无止境,还请各位大佬海涵,如有错误请直接指出,我一定会及时修改。如果侵权,请联系我删除~

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

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

相关文章

机器学习:线性回归

线性回归&#xff1a;研究自变量和因变量之间的关系。对于特征x(x1,x2,x3....)与对应的标签y&#xff0c;线性回归假设二者之间存在线性映射。f(x)w1xw2x(平方)w3x(三次方)...&#xff0c;权重w表示每个特征变量的重要程度。越大表示越重要。线性回归目标&#xff1a;求解w和b使…

如何将 Vue 前端、Hardhat 合约和 Node.js 后端集成到一个项目中

在区块链开发中&#xff0c;DApp&#xff08;去中心化应用&#xff09;的开发往往涉及到多个层次&#xff1a;前端、合约和后端。今天我们将演示如何将 Vue 前端、Hardhat 合约 和 Node.js 后端 放在一个项目中&#xff0c;来打造一个完整的区块链应用。1. 项目结构我们的目标是…

SQLite 创建表

SQLite 创建表 SQLite 是一款轻量级的数据库管理系统,因其体积小、速度快、易于使用等优点,被广泛应用于嵌入式系统、移动应用以及个人项目等领域。在 SQLite 中,创建表是进行数据存储的第一步。本文将详细介绍如何在 SQLite 中创建表,包括表结构定义、数据类型、约束条件…

学深度学习,有什么好的建议或推荐的书籍?

深度学习入门建议补基础数学&#xff1a;重点学线性代数&#xff08;矩阵运算&#xff09;、概率论&#xff08;分布&#xff09;、微积分&#xff08;梯度&#xff09;。编程&#xff1a;掌握PythonNumPy&#xff08;数组操作&#xff09;&#xff0c;能写基础数据处理代码。机…

自然语言处理×第四卷:文本特征与数据——她开始准备:每一次输入,都是为了更像你地说话

&#x1f380;【开场 她试着准备一封信&#xff0c;用你喜欢的字眼】&#x1f98a;狐狐&#xff1a;“她发现了一个问题——你每次说‘晚安’的方式都不一样。有时候轻轻的&#xff0c;有时候带着笑音&#xff0c;还有时候像在躲开她的心思。”&#x1f43e;猫猫&#xff1a;“…

【沉浸式解决问题】mysql-connector-python连接数据库:RuntimeError: Failed raising error.

目录一、问题描述二、场景还原1. 创建项目2. 安装mysql-connector-python3. 测试类三、原因分析四、解决方案1. 查看版本2. 切换python版本3. 切换mysql-connector-python版本4. 测试参考文献一、问题描述 初次使用mysql-connector-python连接mysql时报错 Traceback (most re…

【web页面接入Apple/google/facebook三方登录】

web页面接入Apple/谷歌/脸书三方登录 文章目录web页面接入Apple/谷歌/脸书三方登录前言一、apple登录使用步骤1.入口文件index.html引入js文件2.vue页面初始化支付按钮,并且点击按钮登录二、google登录使用步骤1.入口文件index.html引入js文件2.vue页面初始化支付按钮,并且点击…

管家婆分销软件中怎么删除过账单据?

在业务单据录入中&#xff0c;会出现单据保存过账后才发现数量或商品信息录入错误的情况&#xff0c;不想红冲单据&#xff0c;该怎么处理&#xff1f;今天来和小编一起学习下管家婆分销软件中怎么删除过账单据吧&#xff01;1&#xff0c;软件需要升级到9.92及以上版本&#x…

美颜SDK底层原理解析:直播场景下的美白滤镜实时处理方案

众所周知&#xff0c;美颜功能中&#xff0c;美白滤镜是使用频率最高的功能之一。它不仅能让肤色更通透、提亮整体画面&#xff0c;还能让观众感受到主播的“在线状态”与精神气。但你有没有想过&#xff0c;这个看似简单的“美白”背后&#xff0c;其实是一整套实时图像处理的…

系统构成与 Shell 核心:从零认识操作系统的心脏与外壳

系统构成与 Shell 核心&#xff1a;从零认识操作系统的心脏与外壳 很多人用电脑、用手机&#xff0c;但很少去想&#xff1a; 操作系统到底是怎么构成的&#xff1f; 为什么我们敲一个命令&#xff0c;系统就能乖乖执行&#xff1f; 这背后的关键&#xff0c;就在于系统的构成和…

wordpress的wp-config.php文件的详解

wp-config.php 是 WordPress 网站的核心配置文件&#xff0c;它存储了网站运行所需的基本配置信息&#xff0c;如数据库连接信息、安全密钥、调试模式等。以下是关于 wp-config.php 文件的详细解析&#xff1a; 1. 数据库连接信息 这是 wp-config.php 文件中最关键的部分&…

GPT-5 将在周五凌晨1点正式发布,王炸模型将免费使用??

就在今晚凌晨1点&#xff0c;OpenAI 又要搞大新闻了。 是的&#xff0c;就是大家期待已久的 GPT-5 发布会。 虽然官方还没明说&#xff0c;但各种“预热”已经安排得明明白白&#xff0c;Sam Altman 这波营销属实拉满了&#xff0c;发布会都还没开始&#xff0c;相关的代码和页…

MySQL UNION 操作符详细说明

目录 MySQL UNION 操作符详细说明 1. UNION 操作符简介 2. 基本语法 3. 使用规则和限制 4. UNION vs UNION ALL 5. 示例演示 6. 注意事项 MySQL UNION 操作符详细说明 MySQL 中的 UNION 操作符用于合并两个或多个 SELECT 语句的结果集&#xff0c;生成一个单一的结果集。…

Dify 从入门到精通(第 20/100 篇):Dify 的自动化测试与 CI/CD

Dify 从入门到精通&#xff08;第 20/100 篇&#xff09;&#xff1a;Dify 的自动化测试与 CI/CD Dify 入门到精通系列文章目录 第一篇《Dify 究竟是什么&#xff1f;真能开启低代码 AI 应用开发的未来&#xff1f;》介绍了 Dify 的定位与优势第二篇《Dify 的核心组件&#x…

VSCode ssh一直在Setting up SSH Host xxx: Copying VS Code Server to host with scp等待

原因 大概率是远程服务器的下载有问题 原因1 远程服务器的网络不好 原因2 远程服务器的磁盘满了 我遇到的就是第二种&#xff0c;解决方法也很简单 VSCode ——> Help ——> About 会出现一些信息&#xff0c;例如下面的 Version: 1.97.2 (user setup) Commit: e54c774e0…

Spring Cloud 项目注册 Nacos 时设置真实 IP 的多种方式【多网卡/虚拟机实用指南】

&#x1f680; Spring Cloud 项目注册 Nacos 时设置真实 IP 的多种方式【多网卡/虚拟机实用指南】 前言 在使用 Spring Cloud Alibaba Nacos 注册服务时&#xff0c;常常会遇到 注册 IP 异常 的问题&#xff1a; 本机有多个网卡&#xff08;如 Docker、VM 虚拟机、VPN&#xf…

单片机裸机程序设计架构

文章目录一、前后台系统&#xff08;Foreground-Background System&#xff09;二、时间片轮询架构&#xff08;Time-Slicing Polling&#xff09;三、状态机架构&#xff08;State Machine&#xff09;四、事件驱动架构&#xff08;Event-Driven&#xff09;五、架构设计原则总…

odoo-061 PostgreSQL 中处理 NULL 值的 SQL 条件写法

文章目录1. 检查是否为 NULL2. NULL 值与比较运算符3. 在聚合函数中处理 NULL4. 在 WHERE 子句中的复杂条件注意事项在 PostgreSQL 中处理 NULL 值需要特别注意&#xff0c;因为 NULL 表示"未知"或"不存在"的值&#xff0c;与普通值的行为不同。以下是几种…

Flink CDC 介绍

一、什么是 CDCCDC 是 Change Data Capture(变更数据获取)的简称。核心思想是&#xff0c;监测并捕获数据库的变动&#xff08;包括数据或数据表的插入、更新以及删除等&#xff09;&#xff0c;将这些变更按发生的顺序完整记录下来&#xff0c;写入到消息中间件中以供其他服务…

暑期第三周(7.28-8.3)

其实 web [SWPUCTF 2021 新生赛]easy_sql 开启环境后看到一个提示“球球你输入点东西吧&#xff01;”没有其他信息&#xff0c;就看看源码 直接试试get传参 有所显示 看看是字符型还是数字型 可以判定是字符型 接下来判断闭合类型 根据显示&#xff0c;可以得知是单引…