線程池分析與設計

線程池

基本功能接口

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
英文地址,請注明出處:http://en.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;可以得知是單引…