在学习过相关的多线程知识后,从更抽象的角度来看待多线程解决问题的方向,其实仍然是典型的生产和消费者的模型。无论是数据计算、存储、分发和任务处理等都是通过多线程这种手段来解决生产者和消费者不匹配的情况。所以,得到的结论就清晰了,生产者和消费者根本不匹配的情况下,包括多线程编程在内的各种手段都是无法解决实际问题的。这一点很简单,可是在实际编程过程中,仍然有很多人专注于细节和业务,忽视了这个问题。
模型的意义在于,在看似不匹配的的场景下,通过模型优化实现匹配。这句话有点绕,举个例子就明白了。比如组装100台机器,一个人一天可以组装一台。那么,小学生都可以知道,要想组装完成这些机器,要么100人同时工作,要么1人干一百天。但是现在人都明白了,使用流水线作业可以极大的提高生产效率,即组装这一台机器有30个环节,那么可以找30个人,每人处理一个环节,那么可能1天就完成了。假如某个环节耗时长,某个环节耗时短,那么可以通过最优路径和分治法再次优化,将短和长多对一对应起来,则速度会更快。
模型的意义本质和其没有不同。那么回到线程池的模型来,线程的模型的意义,就是让线程池的使用更高效。那么既然提到模型,其实大家可以很容易的想到,模型的数量肯定不会多,至少在抽象层次的模型上,不会很多。
一般来说,线程池的模型有两大类:
1、领导者-追随者模型
即Leader-Follower模型,既线程池的线程有三种身份,领导者(Leader),追随者(Follower)和工作者(Worker),整个线程池中只有一个领导者,负责管理各种动作,当有事件发生时,其通知Follower们选出一个新Leader,然后自己转变成Worker干活,干活完成重新成为Follower;追随者是线程池启动时除Leader之外的所有线程,它们类似于侯选人,随时等待成为领导者。
这种模型适合于高并发,频繁的小任务动作,比如IM中的聊天信息和HTTP中的打开网页等等。如果任务的耗时长,数据量大等情况时就不适合了。它主要是通过优化线程间的数据复制和上下文调度以及可以增加缓存的命中率等来提高处理的效率。
2、半同步半异步模型
即HA/HS(Half-Sync/Half-Async),即使用线程池处理并发,一部分使用异步,一部分使用同步。比如在IO处理中,IO可以使用异步,但数据任务可以使用队列同步控制。也就是说,整个HA/HS分为三层,异步IO层(与IO异步通信),队列缓冲层(数据任务缓存),同步处理(从队列同步获取数据并处理)。
最终数据会通过异步(回调、消息、事件等)或者同步(管道、返回值等)等待由最上层的客户端拿到数据。
这种模型的变种非常多,因为实际应用的场景非常多。很多开发者为了更好的适应自己的应用场景对其进行各种改变。而且异步IO的机制本身就在不同的平台的实现有着各种情况,比如人们经常提到的前摄器(Proactor)和反应器(Ractor)。
通过描述就明白,在实际的应用场景中,这种方式非常多,换句话说,这种模型基本可以全覆盖,如果实在效率不满足可以再替换成LF模型。
本篇重点分析一下Leader-Follower模型。最典型的线程池的应用场景莫过于服务端的高并发编程了,先看一下其转换图:
在这种模型中,需要注意的是Leader的控制是通过锁来实现的,它有两种机制的方法来实现,一种是直接选举,只有成了领导者进行监听处理,在Ngix中就有类似的实现;另外一种就是使用一个线程竞争得到等待事件的调度,等到后自动升级到Leader。不管如何实现,需要明白是,Leader只有一个,一山只能容一虎嘛。
明白了LF的基本信息,下面看一个基本的线程池的应用:
// LF-Threadpool.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <thread>
#include <mutex>
#include <memory>
#include <vector>
#include <condition_variable>
#include <atomic>
#include <Windows.h>
#include <mutex>
#include <condition_variable>
constexpr int kTHREAD_NUM = 3;
thread_local int LEADER = 0;
class ThreadCondition
{
public:
ThreadCondition() {}
~ThreadCondition() {}
public:
inline bool Wait(int timeOut)
{
signaled_ = false;
std::unique_lock<std::mutex> lock(this->lockMutex_);
if (this->cvLock_.wait_for(lock, std::chrono::milliseconds(timeOut)) == std::cv_status::timeout)
{
return false;
}
return true;
}
inline void Wait()
{
signaled_ = false;
std::unique_lock<std::mutex> lock(this->lockMutex_);
while (!signaled_)
{
this->cvLock_.wait(lock);
}
}
inline void Signal()
{
std::unique_lock<std::mutex> lock(this->lockMutex_);
signaled_ = true;
//pthread_cond_broadcast(&_cond);
this->cvLock_.notify_one();
}
void SetSignal(bool quit = false)noexcept
{
//设置退出循环标志
if (quit)
{
this->quit_ = true;
}
//唤醒线程
this->Signal();
}
private:
bool signaled_ = false;
std::mutex lockMutex_;
std::condition_variable cvLock_;
bool quit_ = false;
};
class ThreadPool
{
public:
ThreadPool() = default;
~ThreadPool()
{
this->tc_.SetSignal(true);
for (auto& pthread : this->vecThread_)
{
if (pthread != nullptr && pthread->joinable())
{
pthread->join();
}
}
}
public:
void init() {
for (int num = 0; num < kTHREAD_NUM; num++)
{
std::unique_ptr<std::thread> pThread =std::make_unique<std::thread>(&ThreadPool::Work,this,num);
this->vecThread_.emplace_back(std::move(pThread));
Sleep(600);
}
}
void Run(int flag)
{
std::cerr<<"thread "<<std::this_thread::get_id() << ",start working......" << std::endl;
Sleep(600);
this->EnterFollower(1);
}
void SelectLeader()
{
this->tc_.Signal();
}
void Work(int flag)
{
LEADER = flag;
auto id = std::this_thread::get_id();
if ( LEADER == 0) {
this->LeaderReady(id);
}
else
{
std::cerr << "cur thread is Follower,thread is :" << id << std::endl;
tc_.Wait();
this->LeaderReady(id);
}
}
void SetWork()
{
tcWorking_.Signal();
}
private:
void EnterFollower(int flag)
{
auto id = std::this_thread::get_id();
LEADER = 1;
std::cerr << "this thread id:" << id << ",cur thread is follower!start wait....." << std::endl;
tc_.Wait();
LEADER = 0;
std::cerr << "this thread id:" << id << " ,become leader!" << std::endl;
}
void LeaderReady(std::thread::id id)
{
std::cerr << "cur thread is Leader,id is:" << id << std::endl;
std::cerr << "wait for work......." << std::endl;
tcWorking_.Wait();
std::cerr << " select next follower become worker!" << std::endl;
SelectLeader();
Run(0);
}
private:
std::vector<std::unique_ptr<std::thread>> vecThread_;
ThreadCondition tc_;
ThreadCondition tcWorking_;
};
class ThreadManager
{
public:
ThreadManager()
{
this->tPool_ = std::make_unique<ThreadPool>();
}
~ThreadManager() = default;
public:
void Start()
{
this->tPool_->init();
}
void SetWorker()
{
std::cerr << "监听到事件,Leader开始工作......" << std::endl;
tPool_->SetWork();
}
private:
std::unique_ptr<ThreadPool> tPool_ = nullptr;
std::mutex mutex_;
std::atomic<bool> quit_ = true;
};
int main()
{
ThreadManager tm;
tm.Start();
Sleep(2000);
tm.SetWorker();//触发主线程工作
Sleep(2000);
tm.SetWorker();
Sleep(2000);
tm.SetWorker();
system("pause");
}
这是一个非常简单的应用,线程启动后有一个线程自动成为Leader,然后其在接收到事件通知后自动通知一个Follower成为Leader,而其自身变成Workder,完成后又转变成Follower,等待机会转变为Leader。如此往复循环,复杂的线程池的应用,基本也是这个框架。
本来是想用上次工程中的代码来实现,但是觉得可能不容易体现出这三个状态的转换,所以就写了一个简单的线程池来体现,线程的等待模拟的是工作时间和触发时间,运行后会发现线程的调度是随机的,但是是在三个线程中轮换的。
也可以将Work函数中的启动就有一个Leader修改为启动均为Follower,然后等待事件通知某一线程升级为Leader。
学习理论就是学习别人抽象出来的知识,然后再把学习到的知识理论应用到自己的工作中。如此往复循环,慢慢就会对这些知识有了更深刻的理解,也就可以在此基础上自己抽象自己的理论和知识体系来指导自己的实际工作。
武林中不是有一句话:“练拳不练功,到老一场空;练功不练拳,到老也枉然!”。计算技术亦是如此。