一切要从一个线程池说起

一切要从一个线程池说起

有一个线程池:
在这里插入图片描述

要能够插入任务,还要将任务分配给多个线程,应该怎么做呢?

首先,要复习基础知识。

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()函数里面原子执行的前两步操作是:

  1. 原子地解锁互斥锁 (unique_lock)
  2. 将当前线程置于等待状态
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,然后在另一个线程中通过 promiseset_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();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值