这个项目采用自实现线程池,相比于qt的全局线程池,这个线程池可以直接获取到返回的future结果,同时支持线程命名。
安全队列
cpp
struct SafeQueue {
bool empty() {
std::shared_lock<std::shared_mutex> const lock(_mutex);
return _queue.empty();
}
size_t size() {
std::shared_lock<std::shared_mutex> lock(_mutex);
return _queue.size();
}
void push(T &ele) {
std::unique_lock<std::shared_mutex> const lock(_mutex);
_queue.push(ele);
}
bool pop(T &ele) {
std::unique_lock<std::shared_mutex> const lock(_mutex);
if (_queue.empty()) {
return false;
}
ele = std::move(_queue.front());
_queue.pop();
return true;
}
设置线程名
windows平台
寻找系统的kernel123模块,寻找设置模块中设置线程名的函数的地址,如果找到了就就这个函数地址转换为可调用的函数,设置线程名。防止有些较老的Windows系统没有这个函数。
Linux平台
强制保持线程名长度在15以下
cpp
static void setThreadName(std::string_view name) {
#ifdef _WIN32
HMODULE kernel32_module = ::GetModuleHandleW(L"kernel32.dll");
if (kernel32_module) {
using SetThreadDescriptionFunc = HRESULT(WINAPI *)(HANDLE, PCWSTR);
auto proc = GetProcAddress(kernel32_module, "SetThreadDescription");
auto set_thread_description_func = reinterpret_cast<SetThreadDescriptionFunc>(reinterpret_cast<void *>(proc));
if (set_thread_description_func) {
std::wstring const w_name = CommonTool::charToWchar(name);
set_thread_description_func(::GetCurrentThread(), w_name.c_str());
}
}
#elif __linux__
if (name) {
// Linux平台设置线程名称,限制长度为15个字符+终止符
std::string threadName(name);
if (threadName.length() > 15) {
threadName.resize(15);
}
pthread_setname_np(pthread_self(), threadName.c_str());
}
#endif
}
原子操作
原子变量atomic有两个安全操作函数
std::automic<bool> temp=false;
store()安全写入temp.store(true);
load()安全读取temp.load(memory_order_...);
两个函数都可以结合memory_order_参数
- memory_order_relaxed:不保证顺序,只保证原子性,最快,但线程间可能看不到最新值。
- memory_order_acquire:用于"读操作",保证本线程后续的所有读写都不会被重排到这个 load 之前。常用于同步"读取"。
- memory_order_release:用于"写操作",保证本线程之前的所有读写都不会被重排到这个 store 之后。常用于同步"写入"。
- memory_order_acq_rel:同时用于读写,结合 acquire 和 release 的语义。
- memory_order_seq_cst:最严格的顺序,保证全局一致性(默认)。
线程池主循环
设置线程名worker+index,有一个原子变量,默认为false,如果为false就持续运行。获取线程池的锁,当条件变量判断线程池的标志为false或者任务队列不为空的时候唤醒,避免忙等待。当成功从任务队列取到一个任务的时候,执行这个任务。
cpp
void operator()() noexcept {
setThreadName(std::string("worker--" + std::to_string(_index)));
bool flag = false;
std::function<void()> func;
while (!_pool->_isShutdown.load(std::memory_order_acquire)) {
{
std::unique_lock<std::mutex> lock(_pool->_mutex);
_pool->_cv.wait(lock, [this]() {
return this->_pool->_isShutdown.load(std::memory_order_acquire) ||
!this->_pool->_taskQueue.empty();
});
flag = _pool->_taskQueue.pop(func);
}
if (flag) {
func();
}
}
}
线程池构造函数
用cpu核心数设置n,将n传给线程池数组初始化数组,循环将数组中的每一个元素初始化为线程对象并绑定私有类worker操作,这样每次创建一个线程会执行woker的循环任务像设置线程名加入任务等。
cpp
explicit ThreadPool(uint32_t n = std::thread::hardware_concurrency()) : _threads(n) {
static uint32_t index = 0;
for (auto &thread : _threads) {
thread = std::thread(Worker(this, index++));
}
spdlog::info("ThreadPool init with {} threads", n);
}
线程池析构函数
停止线程池运转,唤醒所有线程,如果可以回收,循环回收所有线程。
cpp
~ThreadPool() noexcept {
_isShutdown.store(true, std::memory_order_release);
_cv.notify_all();
for (auto &thread : _threads) {
if (thread.joinable()) {
thread.join();
}
}
}
提交任务函数
定义一个模板函数,接受任意类型函数以及任意数量参数,指定返回值类型为std::future,decltype自动推导传入函数类型。用std::forward将任务函数完美转发给lamda使用,类似移动语义。将任务函数包装成packaged_task,再将任务对象转换成可进入线程池的function<void()>类型,加入到任务队列,返回任务的future,方便后续访问结果。
cpp
template <typename F, typename... Args>
auto submit(F &&function, Args &&...args) -> std::future<decltype(function(args...))> {
std::function<decltype(function(args...))()> func = [new_function = std::forward<F>(function), args...]() {
return new_function(args...);
};
// 使用packaged_task获取函数签名,链接func和future,使之能够进行异步操作
auto task_ptr = std::make_shared<std::packaged_task<decltype(function(args...))()>>(func);
// 包装可进入线程的线程函数
std::function<void()> wrapper_func = [task_ptr]() { (*task_ptr)(); };
{
const std::unique_lock<std::mutex> lock(_mutex);
// 将包装的函数加入队列
_taskQueue.push(wrapper_func);
}
// 唤醒一个线程
_cv.notify_one();
// 返回前面注册的任务指针
return task_ptr->get_future();
}
};