一、HA/HS
半同步半异步模型即HA/HS(Half-Sync/Half-Async),即使用线程池处理并发,一部分使用异步,一部分使用同步。但是在实际的开发中,这种模型的变种极多,可以说是眼花缭乱。HA/HS的异步,指的是IO的异步,同步指的是数据或者任务同步从队列控制,两步,就意味着一定有锁。
有锁,基本就是初学者和很多程序员的不爽之处了,下面对其进行分析。
二、分析说明
一般来说,在Linux平台,纯IO异步确实不多,所以更多的IO采用的其实是IO多路复用技术,如常见的Select和epoll等。也就是说在Linux上一般来说应该是反应器(有的叫反应堆)方式,所以其实可以叫做HR/HA。
另外一个重点是同步这部分,这部分一般使用队列实现,队列内可以是任务,当然也可以是数据,这个可以根据实际情况来。其实HA/HS接近于生产者和消费者的模型,也就是说更容易被初学者接受,这也是HA/HS模型应用广泛的一个重要原因。
使用同步队列有一个重要问题,就是线程的惊群问题。即一个事件的到来通知,引起N个等待线程的唤醒但实际只能有一个线程工作的问题。这个在高并发的网络服务端编程是一个绕不开的话题。虽然理论上讲在Linux2.6版本上已经解决上网线Accept的惊群问题,本文中的这种模型中,还有更多的应用场景,其实也会有惊群的问题。
要想解决问题,就要先看一看别人解决问题的手段,内核中解决Accept惊群的问题是增加了一个唤醒标记(WQ_FLAG_EXCLUSIVE ),如果已经有唤醒的则不再唤醒下一个。而在Ngnix中,则有一个互斥锁,进程都来抢,抢到的就类似于leader,可以监听干活,没抢到就只能等待下一次(当然这里面还有其它一些细节,比如延时时长和负载均衡)。这些都是比较优秀的解决方案。
那么既然同步是有锁造成的可不可以使用无锁队列呢?当然可以。但问题是,并不是所有的场景都适合于无锁的情况,这就需要开发者根据情况自己决定。
原则上来讲,解决同步引起的惊群有三个方向:
1、分治法,即将整个工作分成适合的甚至是动态调整的N个分区,最简单的暴力的就是直接生成N个队列。这在前面的并行编程中也提到。
2、无锁化,这需要根据场景自己选择。
3、分级等待(临界区法),即如上面所讲使用锁或者变量控制等待级别形成层次。
各种方法的解决的思路略有不同,但产生的问题是一样的:
1、负载均衡的问题,即不能出现线程的冷热不均,有的忙得要死,有的则闲得要死(特别是涉及到多核,有的核心占百分百,有的则基本是零)
2、效率的问题(响应问题),即不能出现因为锁导致整体的性能有明显的下降或者出现事件处理的丢失现象
另外,此处只讨论同步问题,不讨论队列处理的推拉模式等的应用场景,有兴趣可以自己看看,毕竟实际工程肯定是离不开的。
三、HA/HS的应用
在写线程池的最初的那个例子,其实就是一个HA/HS的模型,下面再延袭LF中的任务写一个例子对比一下就会更明白:
#include <iostream>
#include <thread>
#include <mutex>
#include <memory>
#include <vector>
#include <condition_variable>
#include <atomic>
#include <Windows.h>
#include <functional>
#include <type_traits>
constexpr int kTHREAD_NUM = 6;
#pragma once
#include <mutex>
#include <condition_variable>
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();
}
void SetSignalAll()
{
cvLock_.notify_all();
}
private:
bool signaled_ = false;
std::mutex lockMutex_;
std::condition_variable cvLock_;
bool quit_ = false;
};
thread_local int Data = 0;
class ThreadPool
{
public:
ThreadPool() = default;
~ThreadPool()
{
for (auto& pthread : this->vecThread_)
{
if (pthread != nullptr && pthread->joinable())
{
pthread->join();
}
}
}
public:
void init() {
for (int num = 0; num < kTHREAD_NUM/2; num++)
{
std::unique_ptr<std::thread> pThread = std::make_unique<std::thread>(&ThreadPool::Work, this, num);
this->vecThread_.emplace_back(std::move(pThread));
Sleep(300);
}
}
void Work(int flag)
{
while (quit_) {
this->tcWorking_.Wait();
auto id = std::this_thread::get_id();
std::cout << "cur thread is:" << id << " start work!" << std::endl;
this->Run(flag);
}
std::cout << std::this_thread::get_id()<< " thread quit!" << std::endl;
}
void SetWork()
{
tcWorking_.Signal();
}
template<typename F>
void SetTask(F&& f)
{
this->vecTask_.emplace_back(std::forward<F>(f));
std::cout << "cur size is:" << vecTask_.size() << std::endl;
}
void SetQuit()
{
this->tcWorking_.SetSignalAll();
this->quit_ = false;
}
private:
void Run(int flag)
{
std::lock_guard<std::mutex> lg(mt_);
if (!vecTask_.empty()) {
auto id = vecTask_.size() - 1;
std::cout << "task size is:" << vecTask_.size() - 1 << std::endl;
auto processFunc = this->vecTask_[id];
this->vecTask_.pop_back();
}
}
private:
std::vector<std::unique_ptr<std::thread>> vecThread_;
ThreadCondition tc_;
ThreadCondition tcWorking_;
std::vector<std::function<void(int)>> vecTask_;
std::mutex mt_;
std::atomic<bool> quit_ = true;
};
class ThreadManager
{
public:
ThreadManager()
{
this->tPool_ = std::make_unique<ThreadPool>();
}
~ThreadManager() = default;
public:
void Start()
{
this->tPool_->init();
for (int num = 0; num < kTHREAD_NUM; num++)
{
this->tPool_->SetTask([&](int t) {
std::cout << "cur pars is:" << t << std::endl;
});
}
}
void SetWorker()
{
std::cerr << "监听到事件,启动工作......" << std::endl;
tPool_->SetWork();
}
void SetQuit() {
this->tPool_->SetQuit(); }
private:
std::unique_ptr<ThreadPool> tPool_ = nullptr;
std::mutex mutex_;
};
int main()
{
ThreadManager tm;
tm.Start();
for (int num = 0; num < kTHREAD_NUM; num++)
{
Sleep(100);
tm.SetWorker();
}
tm.SetQuit();
}
掌握好基本的知识,然后再多看一些书籍资料,融合到实际开发中,多次反复后,就会发现,对这些模型的掌握会飞速的提高并有了自己的深入的理解。此时如果再有志同道合者多加讨论,则技术提升的会更高,理解亦会更加深刻。
- 上次的LF代码在退出时有些问题,可参看此次代码
四、总结
在目前可预见的技术范围内,完全无锁的可能性几乎是零,而且相对于有锁编程,无锁编程的要件有点多,这也导致无锁编程更多的应用于基础层。而且大多的应用场景其实对于效率的敏感性并没有到不可忍受的地步,所以这也是HA/HS应用非常广泛的原因。
大家可以看看这篇文章“An Architectural Pattern for Efficient and Well-structured Concurrent I/O”,会有更好的体会心得。