C++11线程池实现

线程池代码

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <queue>
using namespace std;

std::mutex print_mutex;
class ThreadPool
{
public:
    ThreadPool(int numThreads):stop_(false){
        for (int i = 0; i < numThreads; i++)
        {
            threads_.emplace_back([this]
                {
                    while (true)
                    {
                        unique_lock<mutex> lock(mtx_);
                        condition_.wait(lock, [this]
                            {
                                return !tasks_.empty() || stop_;
                            });
                        if (stop_ && tasks_.empty())
                        {
                            return;
                        }
                        //当前任务队列中不为空,就从中取出一个任务开始执行
                        std::function<void()> task(std::move(tasks_.front()));
                        tasks_.pop();
                        lock.unlock();
                        task();
                    }
                });
        }
    }
    ~ThreadPool() {
        {
            unique_lock<mutex> lock(mtx_);
            stop_ = true;
        }
        condition_.notify_all();//如果要析构了,就通知所有等待的线程起来干活
        for (thread& thread : threads_)
        {
            thread.join();//等待所有线程执行完成
        }
    }
    template<typename F,typename...Args>
    void addtasks(F&& f,Args&& ...args)//这里使用的是万能引用。Args表示可以带多个参数
    {
    	//forward称为完美转发类型,保持实参task的左引用或者右引用类型(根据传入判断)
        function<void()> task = bind(forward<F>(f), forward<Args>(args)...);
        
        {
            unique_lock<mutex> lock(mtx_);
            //move返回传入的实参task的右值引用类型
            tasks_.emplace(move(task));
        }
        condition_.notify_one();
    }
private:
    vector<thread> threads_;//线程池
    queue<function<void()>> tasks_;//任务队列
    mutex mtx_;                    //互斥变量
    condition_variable     condition_; //条件变量
    bool stop_;
};

int main()
{
    ThreadPool threads(5);
    for (int i = 0; i < 10; i++)
    {
        threads.addtasks([i] {
            {
                //这里的print_mutex是为了保证打印不会乱序
                //跟这里的线程池没有关系
                std::lock_guard<std::mutex> lock(print_mutex); 
                cout << "task: " << i << "is running" << endl;
            }
           
    
            this_thread::sleep_for(chrono::seconds(1));

            {
                std::lock_guard<std::mutex> lock(print_mutex);
                cout << "task: " << i << "is done" << endl;
            }
           
            });
    }
    return 0;
}

实验结果如下:
在这里插入图片描述

这里实现了一个简单的线程池,在main函数中循环添加了10个任务,然后依次从线程池里面取出线程来执行任务。这里创建了五个线程,用以执行分发的任务,属于单生成者-多消费者模型。
接下来我们就尝试画图分析,线程池实现的原理

初始化线程池

ThreadPool threads(5)
当执行这句话的时候,创建了5个线程池,此时任务队列为空,如图所示
在这里插入图片描述

这里的addtask作为生产者,线程池中的线程作为消费者,不过此时生产者还没有生产任务,消费者没有可消费的任务。即线程池中的线程一直处于wait()等待状态,工作线程为空。

生产者生产一个任务

在这里插入图片描述

当生产者每生产一个任务后,并且成功放入到任务队列之后,就通过notify_one通知消费者(线程池),我已经生产了一个任务,你们谁来处理呢?
在这里插入图片描述

此时notify_one通知了线程池中等待的线程,通过操作系统的调度,假设唤醒了线程3,线程3拿到了资源锁,线程3就从wait状态中退出,开始工作,首先从任务队列中取出任务,并且释放资源锁(线程3从任务队列取出任务后就不需要锁了),开始执行取出的任务,如下图所示。
在这里插入图片描述

生产者又生产了一个任务

假设有另一个任务到来,我们分析一下线程3是否能够执行这个新的任务,以下是可能出现的情况。

新任务到来时,线程3还没有执行完成

  1. 此时线程3还在执行 task()任务;并且任务队列mtx_已解锁;
  2. 主线程成功获取锁,添加任务到任务队列中去,随后调用 notify_one();
  3. 唤醒的是线程池中处于 wait 状态的线程,比如说线程5;
  4. 线程5从任务队列中取出任务,并执行任务

线程3此时仍然在工作,不处于等待状态,所以不会被唤醒,自然也不产生竞争。

新任务到来时,线程3恰好执行完成

线程3恰好执行任务队列后,任务队列为空,此时来了一个新任务

task();//线程3恰好执行完成这个函数
unique_lock<mutex> lock(mtx_); //进入下一个循环,抢占锁
CaseA
  1. 线程3与生产者竞争,并且线程3成功抢到锁
  2. 此时线程3发现任务队列为空,执行cv.wait(),进入等待状态
  3. 生产者随后获取了资源锁,添加任务,并且调用notify_one()唤醒等待线程
  4. 有可能线程3被再次唤醒,执行这个任务(此时线程池中的所有线程都有可能被唤醒)
CaseB

线程3与生产者竞争,生产者抢到锁了;生产者往任务队列中添加任务,并且调用notify_one唤醒线程池中等待线程,假设准备唤醒线程5。这样存在两种情况

CaseB1

  1. 线程3(此时还是活跃线程),并且在线程(如线程5)被唤醒前就已经获取了资源锁
  2. 线程3从任务队列中取出任务,并执行任务
  3. 线程5被唤醒后发现任务队列为空,重新陷入等待状态

CaseB2

  1. 等待线程(如线程5)被成功唤醒
  2. 线程3和线程5竞争抢占资源锁,抢占成功的一方负责执行任务,抢占失败的线程进入等待状态
  3. 成功执行完任务的线程也进入等待状态

tips:这个恰好执行完成的度不好把握,如果是线程3刚刚执行完task()任务并退出,实际上是不可能等到主线程添加了任务,唤醒线程池中等待线程,等待线程与活跃线程3竞争锁的;更可能的方式是,将要执行完task(),会存在上述的一个情况,所以笼统的说恰好执行完task这个任务也是不准确的。

新任务到来时,线程3已经执行完成

此时所有线程都是处于等待状态的,由操作系统调度唤醒某个线程。

生产者生产快于消费者消费

下图是生产者生产的过快,消费者来不及处理任务队列中的数据。
在这里插入图片描述
上述是我分析了线程池中可能存在的一些情况,如果有误,敬请斧正。

知识点剖析

生产者-消费者模型

在这里插入图片描述

特点
• 如果队列里的生产产品已满,生产者就不能继续生产;
• 如果队列里的产品从无到有,生产者需要通知一下消费者,告诉它可以来消费了;
• 如果队列里已经没有产品了,消费者也无法继续消费;
• 如果队列里的产品不满,消费者也得去通知下生产者,说你可以来继续生产了。

优势

解耦合:将生产者类和消费者类进行解耦,消除代码之间的依赖性,简化工作负载的管理。
复用:通过将生产者类和消费者类独立开来,那么可以对生产者类和消费者类进行独立的复用与扩展。
调整并发数:由于生产者和消费者的处理速度是不一样的,可以调整并发数,给予慢的一方多的并发数,来提高任务的处理速度。
异步:对于生产者和消费者来说能够各司其职,生产者只需要关心缓冲区是否还有数据,不需要等待消费者处理完;同样的对于消费者来说,也只需要关注缓冲区的内容,不需要关注生产者,通过异步的方式支持高并发,将一个耗时的流程拆成生产和消费两个阶段,这样生产者因为执行 put() 的时间比较短,而支持高并发。
支持分布式:生产者和消费者通过队列进行通讯,所以不需要运行在同一台机器上,在分布式环境中可以通过 redis 的 list 作为队列,而消费者只需要轮询队列中是否有数据。同时还能支持集群的伸缩性,当某台机器宕掉的时候,不会导致整个集群宕掉。

生产者-消费者模型:理论讲解及实现(C++)
多线程必考的「生产者 - 消费者」模型

condition_varible

对于condition_variable::wait函数的理解

condition_variable::wait提供了两个重载函数

 void wait(unique_lock<mutex>& _Lck) { // wait for signal
        // Nothing to do to comply with LWG-2135 because std::mutex lock/unlock are nothrow
        _Cnd_wait(_Mycnd(), _Lck.mutex()->_Mymtx());
    }

    template <class _Predicate>
    void wait(unique_lock<mutex>& _Lck, _Predicate _Pred) { // wait for signal and test predicate
        while (!_Pred()) {
            wait(_Lck);
        }
    }

(1)void wait( std::unique_lock<std::mutex>& _Lck)

调用底层函数_Cnd_wait函数实现了原子操作(解锁unlock(mutex对应的资源)、阻塞当前的执行线程)。把当前线程添加到等待线程列表中,该线程会持续阻塞直到被 notify_all() 或 notify_one() 唤醒。被唤醒后,该线程会重新获取mutex(竞争拿到该mutex资源),获取资源后执行下一步操作。

(2)void wait(unique_lock<mutex>& _Lck, _Predicate _Pred)

这个重载设置了第二个参数 _Pred, 只有当_Pred为false时,wait才会阻塞当前线程。

【C++】多线程condition_variable的wait

对于condition_variable::notify_one函数的理解

void notify_one() noexcept;
void notify_all() noexcept;

如果有线程在等待 *this,则调用 notify_one 将解除阻塞其中一个等待线程。
notify_all是解除所有等待线程。

条件变量操作的原子性保证
wait()系列操作(wait() / wait_for() / wait_until()) 和 notify_one()/notify_all()操作,包括三个原子步骤:解锁+等待、唤醒与重新加锁,都在一个全局顺序中执行。
这个顺序由条件变量内部维护,类似于原子变量的修改顺序。
实现效果为:notify_one() 不可能被延迟到"在通知后开始等待"的线程之后执行
如:如果线程A调用 notify_one() 后,线程B才开始调用 wait(),那么B不会被这次通知唤醒

通知时是否应该持有锁?

// 情况1:不持有锁通知(推荐)
{
    std::lock_guard lock(mutex);
    // 更新共享状态
} // 自动解锁
cv.notify_one(); // 通知时未持有锁

// 情况2:持有锁通知(特定场景需要)
{
    std::lock_guard lock(mutex);
    // 更新共享状态
    cv.notify_one(); // 通知时仍持有锁
}

通知时一般不持有锁,原因是被唤醒的线程会立即尝试获取锁,而如果通知线程仍持有锁,会导致"唤醒即阻塞"的低效场景。

lambda的理解

lambda表达式是一种匿名函数,主要用于表示简单的行为或者代码块,并在需要时传递到其他函数中。Lambda表达式的基本语法如下:

[capture list] (parameter list) -> return type { function body }

  • capture list 是捕获列表,用于指定 Lambda表达式可以访问的外部变量。捕获列表可以为空,表示不访问任何外部变量,也可以使用默认捕获模式 & 或 = 来表示按引用或按值捕获所有外部变量等。
  • parameter list 是参数列表,用于表示 Lambda表达式的参数,可以为空,表示没有参数,也可以和普通函数一样指定参数的类型和名称。
  • return type 是返回值类型,用于指定 Lambda表达式的返回值类型,可以省略,表示由编译器根据函数体自动推导,也可以使用 -> 符号显式指定。
  • function body 是函数体,用于表示 Lambda表达式的具体逻辑。

深入浅出 C++ Lambda表达式:语法、特点和应用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值