一切要从一个线程池说起
有一个线程池:

要能够插入任务,还要将任务分配给多个线程,应该怎么做呢?
首先,要复习基础知识。
1、线程thread
(1)构造函数
以下是他的初始化构造函数
template<class Fn, class... Args>
explicit thread(Fn&& fn, Args&&... args);
可以看出既可以接收左值,也可以接收右值。
复习一下解包和打包:
- 打包时(声明): … 放在被打包元素的后面。
- 类型包:
typename...Args(… 在typename后面) - 非类型包:
int...Nums(… 在int后面) - 解包时(使用): … 放在包名称的后面。
- print(
args...); (… 在包名args后面)
也就是说,thread可以接收一个函数指针(Fn或者&Fn皆可,&Fn会做隐式转换)以及多个函数的参数。
thread的拷贝函数是被delete的,因为想想也不可能有两个pid和栈资源相同的线程,不是乱套了。
所以thread只能有移动语义,即
t3 = std::move(t2);
此时t2会变为空,t3获取t2之前拥有的所有资源。
(2)常用函数
1、get_id()
获取线程ID,返回类型std::thread::id对象。
http://www.cplusplus.com/reference/thread/thread/get_id/
2、joinable()
判断线程是否可以加入等待,只要线程已经被join或者detach了,此值变为false。
http://www.cplusplus.com/reference/thread/thread/joinable/
3、join()
等该线程执行完成后才返回。
http://www.cplusplus.com/reference/thread/thread/join/
4、detach()
http://www.cplusplus.com/reference/thread/thread/detach
当主线程(或任何创建了子线程的线程)结束时,如果子线程还没有被 join() 或 detach(),那么整个程序会强制终止,所有子线程也会随之结束,而不会正常执行完毕。
其实这是因为主线程和进程生命周期一般是相同的,进程结束才导致所有资源被回收,而不是主线程直接影响了子线程。
std::thread 对象的生命周期管理,必须通过 join() 或 detach() 来完成,否则程序会因为未处理的线程而异常终止。
join() 的本质是同步等待并清理。
detach() 的本质是异步分离并清理(将清理的责任转交给操作系统)。
detach后,连getpid都做不到,因为该子线程已经交给操作系统,无法通过句柄管理了。同时,join完子线程已被清理,自然也无法被管理。
5、std::ref()
如果想传入引用,必须调用std::ref()让程序知道
std::thread t3(func3, std::ref(c));
因为thread无法保证自己的生命周期和传入变量是一致的,所以默认会值拷贝,否则如果访问已经被销毁的变量,则会造成悬空引用
所以使用std::ref()包装器,通过创建一个可拷贝的std::reference_wrapper对象,巧妙地绕过了std::thread的值拷贝机制,实现了安全的引用传递。
6、yield
相当于让出cpu,并不保证同步等,让出来以后自己进入就绪状态,后面cpu调用或者不调用自己,完全看cpu自己咋分配。
基本平时用不到。
2、互斥量
常用的就是mutex,顾名思义,同一个时间只有一个线程可以获得mutex,其他线程都得等着这个线程释放mutex。
lock()上锁,unlock()解锁,try_lock()尝试上锁。
常用的就是lock_guard()和unique_lock()
(1) lock_guard()
其实可以简单的理解为封装了一个类来管理mutex。lock_guard本质是在栈上的,构造时lock,析构时unlock,也是一种RAII。
在绝大多数简单上锁解锁的场景中,优先使用 std::lock_guard。只有当需要高级功能时,才考虑使用 std::unique_lock。
void print_thread_id (int id) {
try {
// using a local lock_guard to lock mtx guarantees unlocking on
destruction / exception:
std::lock_guard<std::mutex> lck (mtx);
print_even(id);
}
catch (std::logic_error&) {
std::cout << "[exception caught]\n";
}
}
(2) unique_lock()
有 lock_guard()的所有功能,同时可以自己上锁解锁,所以只有unique_lock可以配合条件变量。因为wait需要释放锁。这个“释放锁-等待-重新加锁”的复杂过程,必须由unique_lock来完成。lock_guard由于无法在中间手动释放锁,所以无法满足wait()函数的要求。
void fun2() {
while (true) {
std::unique_lock<std::mutex> locker(mu);
cond.wait(locker, [](){return !q.empty();});
auto data = q.back();
q.pop_back();
// locker.unlock(); // 这里是不是必须的?
std::cout << "thread2 get value form thread1: " << data << std::endl;
}
}
(3) 条件变量
条件变量condition_variable是为了保证线程同步,即线程A完成后,通知线程B继续做,如果单纯通过互斥量是没有办法保证先后顺序的,毕竟谁抢到锁就算谁的。只有条件变量可以保证线程间的协作。
介绍几个重要概念:
1、wait
wait函数可以先解锁,等待通知(notify),获得通知后先抢锁,判断谓语是否返回true,如果返回true就返回,否则释放锁重新等待通知,(最后一步只有wait有第二个参数才会执行,否则直接抢锁然后返回,可能会导致虚假唤醒,所以一般都有第二个参数)。
wait()函数里面原子执行的前两步操作是:
- 原子地解锁互斥锁 (
unique_lock) - 将当前线程置于等待状态
void wait (unique_lock<mutex>& lck);
template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);
2、wait_for、wait_until 其实就是加了一个超时也返回的机制。
3、notify_one()、notify_all() ,第一个解锁一个线程,第二个解锁所有线程。
(4)原子变量
1. 原子性(Atomicity)
这是原子变量最核心的特性。一个操作是“原子”的,意味着它是一个不可分割的整体,要么完全执行,要么完全不执行。在多线程环境中,这保证了当一个线程正在对原子变量进行读、写或修改时,其他线程无法中断这个操作。这从根本上杜绝了数据竞争(data race),因为它保证了任何时候对共享变量的操作都是完整的,不会出现只完成了一半的情况。
例如,std::atomic<int> counter; 的 counter++ 操作:
- 非原子操作:在普通变量上,
++实际上包含“读取”、“加1”和“写入”三个步骤,这三步可能被线程切换打断。 - 原子操作:在原子变量上,
++会作为一个单一的、不可分割的指令来执行,保证了最终结果的正确性。
2. 内存序(Memory Order)
虽然原子操作本身是不可分割的,但现代编译器和CPU为了优化性能,可能会对指令进行重排。如果没有正确的同步机制,一个线程对原子变量的操作顺序,可能在另一个线程看来是乱序的。
std::atomic 允许你通过**内存序(std::memory_order)**来控制这种重排行为,从而建立线程间的同步和可见性。常用的内存序有:
relaxed:最弱的内存序,只保证操作的原子性,不提供任何跨线程的顺序保证。acquire:用于读操作,保证此操作之后的所有内存访问不会被重排到它前面。release:用于写操作,保证此操作之前的所有内存访问不会被重排到它后面。seq_cst:最强的内存序(默认),提供顺序一致性,保证所有线程都以相同的顺序看到所有原子操作。
3、异步操作
std::future : 异步指向某个任务,然后通过future特性去获取任务函数的返回结果。
std::aysnc:是一个高层级的接口,异步运行某个任务函数,帮你启动任务并获取 future。
std::packaged_task :是一个中层级的接口(函数对象),帮你打包任务和 future,是一种封装对任务的封装。
std::promise:是一个低层级的接口,用于手动设置结果,然后通过 future 传递。
3.1 future
通过异步操作,可以放心的阻塞等待异步结果返回。一般的用法就是通过一些方式获得std::future<T>变量(如std::packaged_task和std::promise使用的是get_future,而std::aysnc直接返回future值),然后通过get这个future的变量来阻塞等待结果。
std::future<int> result = std::async(find_result_to_add);
// std::future<decltype (find_result_to_add())> result =
std::async(find_result_to_add);
// auto result = std::async(find_result_to_add); // 推荐的写法
do_other_things();
std::cout << "result: " << result.get() << std::endl; // 延迟是否有影响?
std::async:这是最简单的方式。它直接启动任务,并自动返回一个 std::future。
std::packaged_task:这是一个更灵活的选项。你将任务函数封装进 packaged_task,然后通过其 get_future() 方法来获取 std::future。你需要手动将 packaged_task 传递给 std::thread 来启动任务。
std::promise:这是最底层的工具。你创建一个 promise,通过 get_future() 获取其 future,然后在另一个线程中通过 promise 的 set_value() 手动设置结果。
3.2 std::aysnc
跟thread类似,async允许你通过将额外的参数添加到调用中,来将附加参数传递给函数。因为其底层默认会创建一个线程。
如果传入的函数指针是某个类的成员函数,则还需要将类对象指针传入(直接传入,传入指针,或者是std::ref封装)。
std::future<int> result = std::async(find_result_to_add);
// std::future<decltype (find_result_to_add())> result =
std::async(find_result_to_add);
// auto result = std::async(find_result_to_add); // 推荐的写法
do_other_things();
std::cout << "result: " << result.get() << std::endl; // 延迟是否有影响?
自然,auto是最好的用于接收anysc返回值的方式,但是这里也要介绍一下decltype,decltype返回既不是左值也不是右值,返回的是一个类型。所以可以用这种用法:
using xxx = decltype(yyy);
如果要获取一个函数的返回值,且该函数本身有传入参数,则需要传入具体的值让decltype推导:
std::future<decltype (find_result_to_add(0, 0))> result =
std::async(find_result_to_add, 10, 10);
底层decltype涉及到惰性推导,分析其签名来推断类型,而不会真正执行它。
3.3 std::package_task
他不会创建一个线程,他只是一种对任务的封装,并不会真正执行任务,std::package_task就是就是一个函数对象。
只有调用此task才会运行。这在设置线程池中要执行的任务是很重要,因为需要输入task交给线程去运行,在这个过程当中,不能直接运行任务。
其模板是函数签名(即包含输入参数和返回值的形式)
//1-5-package_task
#include <iostream>
#include <future>
using namespace std;
int add(int a, int b, int c)
{
std::cout << "call add\n";
return a + b + c;
}
void do_other_things()
{
std::cout << "do_other_things" << std::endl;
}
int main()
{
std::packaged_task<int(int, int, int)> task(add); // 封装任务
do_other_things();
std::future<int> result = task.get_future();
task(1, 1, 2); //必须要让任务执行,否则在get()获取future的值时会一直阻塞
std::cout << "result:" << result.get() << std::endl;
return 0;
}
3.4 std::promise
很简单,一个地方手动设置值了,在这之后通过相关联的std::future对象进行读取。也是异步的。
//1-5-promise
#include <future>
#include <string>
#include <thread>
#include <iostream>
using namespace std;
void print(std::promise<std::string>& p)
{
p.set_value("There is the result whitch you want.");
}
void do_some_other_things()
{
std::cout << "Hello World" << std::endl;
}
int main()
{
std::promise<std::string> promise;
std::future<std::string> result = promise.get_future();
std::thread t(print, std::ref(promise));
do_some_other_things();
std::cout << result.get() << std::endl;
t.join();
return 0;
}
4、function和bind的用法
function可以用来保存普通函数,lambda表达式以及成员函数。
//保存普通函数
void func1(int a)
{
cout << a << endl;
}
//1. 保存普通函数
std::function<void(int a)> func;
func = func1;
func(2); //2
std::function<void()> func_1 = [](){cout << "hello world" << endl;};
func_1(); //hello world
//保存成员函数
class A{
public:
A(string name) : name_(name){}
void func3(int i) const {cout <<name_ << ", " << i << endl;}
private:
string name_;
};
//3 保存成员函数
std::function<void(const A&,int)> func3_ = &A::func3;
A a("darren");
func3_(a, 1);
bind主要就是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象(其实就是函数对象)来“适应”原对象的参数列表。如果遇到多个同名重载函数,则需要声明传入的可调用对象的类型,否则会造成编译错误:
// 强制转换为函数指针
using FuncType = void(*)(int);
FuncType ptr = func;
// bind 使用这个明确的函数指针
auto bound_func = std::bind(ptr, 10);
可以只指定其中一个位置的参数,如下是第三个位置强制为3:
auto f2 = std::bind(fun_1, placeholders::_1, placeholders::_2, 3);
类函数自然要传入对象地址(this指针):
A a;
a.a = 10;
//f5的类型为 function<void(int, int)>
auto f5 = std::bind((void(A::*)(int, int))A::fun_3, &a, 40, 50); //使用auto关键字
所有的概念都看完了,接下来要回归到线程池了:

5、线程池
主线程可以通过如下方式来初始化,调用以及退出线程池:
void test1() // 简单测试线程池*
{
ZERO_ThreadPool threadpool; *// 封装一个线程池*
threadpool.init(1); *// 设置线程的数量*
threadpool.start(); *// 启动线程池,创建线程, 线程没有start,创建完毕后被调度*
*// 假如要执行的任务*
*// threadpool.exec(1000,func0); // 1000是超时1000的意思,目前没有起作用*
threadpool.exec(**func1**, 10);
*// threadpool.exec((void(\*)(int))func1, 10); // 插入任务*
*// threadpool.exec((void(\*)(string))func1, "king");*
threadpool.exec(**func2**, 20, "darren"); *// 插入任务*
threadpool.waitForAllDone(); *// 等待都执行完退出 运行函数 插入1000个任务, 等1000个任务执行完毕才退出*
threadpool.stop(); *// 这里才是真正执行退出*
}
init() 只是单纯地设置线程数量:
bool ZERO_ThreadPool::init(size_t *num*)
{
std::unique_lock<std::mutex> lock(mutex_);
if (!threads_.empty())
{
return false;
}
threadNum_ = *num*; *// 设置线程数量*
return true;
}
start() 创建新线程,任务设为run,并且push到队列中(emplace_back应该更好):
bool ZERO_ThreadPool::start()
{
std::unique_lock<std::mutex> lock(mutex_);
if (!threads_.empty())
{
return false;
}
for (size_t i = 0; i < threadNum_; i++)
{
threads_.push_back(new thread(&ZERO_ThreadPool::run, this));
}
return true;
}
run本身会去读取任务(get),如果读取到了,则让原子量++(是为了确保后面的notify_all是在所有任务完成之后才执行的),然后执行任务,所有任务执行完了,则通知waitForAllDone:
void ZERO_ThreadPool::run() *// 执行任务的线程*
{
*//调用处理部分*
while (!isTerminate()) *// 判断是不是要停止*
{
TaskFuncPtr task;
bool ok = get(task); *// 1. 读取任务*
if (ok)
{
++atomic_;
try
{
if (task->_expireTime != 0 && task->_expireTime < TNOWMS )
{
*//超时任务,是否需要处理?*
}
else
{
task->_func(); *// 2. 执行任务*
}
}
catch (...)
{
}
--atomic_;
*//任务都执行完毕了*
std::unique_lock<std::mutex> lock(mutex_);
if (atomic_ == 0 && tasks_.empty()) *// 3.检测是否所有任务都运行完毕*
{
condition_.notify_all(); *// 这里只是为了通知waitForAllDone*
}
}
}
}
get就是典型的条件变量等待(只有在插入任务(exec)以及任务结束(stop)时wait会成返回)。任务结束(stop)时wait会成返回,主要是防止run出现异常。
bool ZERO_ThreadPool::get(TaskFuncPtr& *task*)
{
std::unique_lock<std::mutex> lock(mutex_); *// 也要等锁*
if (tasks_.empty()) *// 判断是否任务存在*
{
condition_.wait(lock, [this] { return bTerminate_ *// 要终止线程池 bTerminate_设置为true,外部notify后*
|| !tasks_.empty(); *// 任务队列不为空*
}); *// notify -> 1. 退出线程池; 2.任务队列不为空*
}
if (bTerminate_)
return false;
if (!tasks_.empty())
{
task = std::move(tasks_.front()); *// 使用了移动语义*
tasks_.pop(); *// 释放一个任务*
return true;
}
return false;
}
waitforalldone有超时等待任务结束的效果:
*// 1000ms*
bool ZERO_ThreadPool::waitForAllDone(int *millsecond*)
{
std::unique_lock<std::mutex> lock(mutex_);
if (tasks_.empty())
return true;
if (*millsecond* < 0)
{
condition_.wait(lock, [this] { return tasks_.empty(); });
return true;
}
else
{
return condition_.wait_for(lock, std::chrono::milliseconds(*millsecond*), [this] { return tasks_.empty(); });
}
}
exec最为复杂,他的作用是包装函数,丢到队列里,并且返回一个异步的future供外部get。
F&& f, Args&&... args用到了万能引用可以完美转发
decltype(f(args...))可以用来推到返回值,并且用using给他起了一个别名RetType。
同时用RetType()函数签名作为packaged_task的模板类型(代表一个无参的函数,毕竟bind生成的也是一个无参可调用对象),用bind生成一个新的可调用对象,同时注意完美转发,保证传入的是左/右值,这里也是左/右值。
同时用智能指针保证资源的管理,因为 task 需要在多个线程之间共享(主线程和工作线程),主线程要get_future,子线程也要调用函数。
在lambda表达式中,捕获列表传入task并且用*取到资源并且利用函数对象的特性进行调用,最后将任务插入并且提醒线程(run函数)处理。
同时返回get_future,供外部get。
template <class F, class... Args>
auto exec(int64_t timeoutMs, F&& f, Args&&... args) -> std::future<decltype(f(args...))>
{
int64_t expireTime = (timeoutMs == 0 ? 0 : TNOWMS + timeoutMs); *// 获取现在时间*
*//定义返回值类型*
using RetType = decltype(f(args...)); *// 推导返回值*
*// 封装任务*
auto task = std::make_shared<std::packaged_task<RetType()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));
TaskFuncPtr fPtr = std::make_shared<TaskFunc>(expireTime); *// 封装任务指针,设置过期时间*
fPtr->_func = [task]() { *// 具体执行的函数*
(*task)();
};
std::unique_lock<std::mutex> lock(mutex_);
tasks_.push(fPtr); *// 插入任务*
condition_.notify_one(); *// 唤醒阻塞的线程,可以考虑只有任务队列为空的情况再去notify*
return task->get_future();;
}
stop就是通知get和run可以结束了,否则一直阻塞在get会导致子线程没法join。
void ZERO_ThreadPool::stop()
{
{
std::unique_lock<std::mutex> lock(mutex_); *//加锁*
bTerminate_ = true; *// 触发退出*
condition_.notify_all();
}
for (size_t i = 0; i < threads_.size(); i++)
{
if(threads_[i]->joinable())
{
threads_[i]->join(); *// 等线程推出*
}
delete threads_[i];
threads_[i] = NULL;
}
std::unique_lock<std::mutex> lock(mutex_);
threads_.clear();
}

1704

被折叠的 条评论
为什么被折叠?



