day11-完善线程池
day10的任务中设计了一个线程池来处理任务,完成单Reactor多线程的模式设计,由于任务队列的添加、取出都存在拷贝操作,线程池不会有太好的性能,只能用来学习,今天就会对线程池进行改进,之前的百行设计线程池的任务文章中有更为复杂的设计,有兴趣可以看一下。
1、错误检测机制
2、地址创建
3、Socket
4、高并发Epoll
关于IO复用的原理已经作为补充内容添加到了day3计算机原理还是比较重要的。
5、Channel
从Channel发生变化,Channel之前的作用是将事件添加到线程池中等待处理(再之前是直接调用回调函数处理事件),现在在读事件的基础上添加了写事件,那么分发事件类型给予不同的处理方式的机制就出现了,先来看看类声明
class Socket;
class EventLoop;
class Channel
{
private:
EventLoop *loop;
int fd;
uint32_t events;
uint32_t ready;
bool inEpoll;
bool useThreadPool;
std::function<void()> readCallback;
std::function<void()> writeCallback;
public:
Channel(EventLoop *_loop, int _fd);
~Channel();
void handleEvent();
void enableRead();
int getFd();
uint32_t getEvents();
uint32_t getReady();
bool getInEpoll();
void setInEpoll(bool _in = true);
void useET();
void setReady(uint32_t);
void setReadCallback(std::function<void()>);
void setUseThreadPool(bool use = true);
};
和之前相比,多了useThreadPool和两个回调函数作为属性,修改了setInEpoll方法多了useET、setUseThreadPool方法,既然多了事件类型,那么handleEvent作为分发到线程处理事件的方法一定发生了很大改变,接下来我们一步一步分析实现。
首先是构造函数和析构函数
Channel::Channel(EventLoop *_loop, int _fd)
: loop(_loop), fd(_fd), events(0), ready(0), inEpoll(false), useThreadPool(true){}
Channel::~Channel(){
if(fd != -1){
close(fd);
fd = -1;
}
}
在handleEvent中需要先判断事件类型,当就绪事件为读事件并存在使用线程池的情况时将读事件加入线程池,不存在使用线程池的情况是直接调用回调函数。
void Channel::handleEvent(){
if(ready & (EPOLLIN | EPOLLPRI)){
if(useThreadPool)
loop->addThread(readCallback);
else
readCallback();
}
if(ready & (EPOLLOUT)){
if(useThreadPool)
loop->addThread(writeCallback);
else
writeCallback();
}
}
将 EPOLLIN(普通的数据可读事件)和 EPOLLPRI(紧急数据可读事件)两个事件标志添加到 events 变量中,再将 EPOLLET(边缘触发模式)事件标志添加到 events 变量中
void Channel::enableRead(){
events |= EPOLLIN | EPOLLPRI;
loop->updateChannel(this);
}
void Channel::useET(){
events |= EPOLLET;
loop->updateChannel(this);
}
后面是祖传的设置属性获取属性的方法,值得注意的是目前只对读事件提供了回调函数方法,也就是今天还没完成写事件的编写
int Channel::getFd(){
return fd;
}
uint32_t Channel::getEvents(){
return events;
}
uint32_t Channel::getReady(){
return ready;
}
bool Channel::getInEpoll(){
return inEpoll;
}
void Channel::setInEpoll(bool _in){
inEpoll = _in;
}
void Channel::setReady(uint32_t _ev){
ready = _ev;
}
void Channel::setReadCallback(std::function<void()> _cb){
readCallback = _cb;
}
void Channel::setUseThreadPool(bool use){
useThreadPool = use;
}
6、EventLoop
7、Acceptor
注意由于目前这个服务器不会出现一瞬间很多连接同时到达,因此采用连接时采用非阻塞反而会导致性能下降,因此,这里将sock->setnonblocking()进行去除。并且没必要将这个时间加入到线程池中,所以acceptChannel->setUseThreadPool(false),并不要设置为ET模式。
Acceptor::Acceptor(EventLoop *_loop) : loop(_loop), sock(nullptr), acceptChannel(nullptr){
sock = new Socket();
InetAddress *addr = new InetAddress("127.0.0.1", 1234);
sock->bind(addr);
// sock->setnonblocking();
sock->listen();
acceptChannel = new Channel(loop, sock->getFd());
std::function<void()> cb = std::bind(&Acceptor::acceptConnection, this);
acceptChannel->setReadCallback(cb);
acceptChannel->enableRead();
acceptChannel->setUseThreadPool(false);
delete addr;
}
8、Connection
这个是用来处理事件并将在事件完成时处理空间的,因为在Channel部分做了修改,这里也需要修改,首先是构造函数,这里将原本的enableRead转换为了enableRead+useET才能完成,分离出来useET是为了后续enableWrite提供简单的方法,之后多了一个设置线程池是否可用的方法调用:
Connection::Connection(EventLoop *_loop, Socket *_sock) : loop(_loop), sock(_sock), channel(nullptr), inBuffer(new std::string()), readBuffer(nullptr){
channel = new Channel(loop, sock->getFd());
channel->enableRead();
channel->useET();
std::function<void()> cb = std::bind(&Connection::echo, this, sock->getFd());
channel->setReadCallback(cb);
channel->setUseThreadPool(true);
readBuffer = new Buffer();
}
之后给出echo方法的实现,今天加入了重置连接的部分(传输过程中的几种状态:中断、读取、断开、重置),但是代码中给出注释会出现bug,很有可能是因为重复deleteConnectionCallback即没有进行合适的同步控制导致的。
void Connection::echo(int sockfd){
char buf[1024]; //这个buf大小无所谓
while(true){ //由于使用非阻塞IO,读取客户端buffer,一次读取buf大小数据,直到全部读取完毕
bzero(&buf, sizeof(buf));
ssize_t bytes_read = read(sockfd, buf, sizeof(buf));
if(bytes_read > 0){
readBuffer->append(buf, bytes_read);
} else if(bytes_read == -1 && errno == EINTR){ //客户端正常中断、继续读取
printf("continue reading\n");
continue;
} else if(bytes_read == -1 && ((errno == EAGAIN) || (errno == EWOULDBLOCK))){//非阻塞IO,这个条件表示数据全部读取完毕
printf("message from client fd %d: %s\n", sockfd, readBuffer->c_str());
// errif(write(sockfd, readBuffer->c_str(), readBuffer->size()) == -1, "socket write error");
send(sockfd);
readBuffer->clear();
break;
} else if(bytes_read == 0){ //EOF,客户端断开连接
printf("EOF, client fd %d disconnected\n", sockfd);
deleteConnectionCallback(sockfd); //多线程会有bug
break;
} else {
printf("Connection reset by peer\n");
deleteConnectionCallback(sockfd); //会有bug,注释后单线程无bug
break;
}
}
}
新东西,send数据方法,利用c_str将数据复制到临时缓存中,data_size获取readBuffer中的长度,data_left 用以确定尚未发送的数据量,由于循环中每次发送的数据可能不是全部数据,因此使用 buf + data_size - data_left 来表示剩余待发送数据在 buf 中的起始地址,data_left 表示剩余待发送数据的大小。
void Connection::send(int sockfd){
char buf[readBuffer->size()];
strcpy(buf, readBuffer->c_str());
int data_size = readBuffer->size();
int data_left = data_size;
while (data_left > 0)
{
ssize_t bytes_write = write(sockfd, buf + data_size - data_left, data_left);
if (bytes_write == -1 && errno == EAGAIN) {
break;
}
data_left -= bytes_write;
}
}
9、Buffer
多了一个将一个buffer推入到当前buf的方法
void Buffer::setBuf(const char* _buf){
buf.clear();
buf.append(_buf);
}
10、ThreadPool
之前的实现中,add仅支持std::function<void()>类型的参数,所以函数参数需要事先使用std::bind(),并且无法得到返回值。
为了解决以上提到的问题,线程池的构造函数和析构函数都不会有太大变化,唯一需要改变的是将任务添加到任务队列的add函数。我们希望使用add函数前不需要手动绑定参数,而是直接传递,并且可以得到任务的返回值。新的声明代码如下:
class ThreadPool
{
private:
std::vector<std::thread> threads;
std::queue<std::function<void()>> tasks;
std::mutex tasks_mtx;
std::condition_variable cv;
bool stop;
public:
ThreadPool(int size = 10);
~ThreadPool();
// void add(std::function<void()>);
template<class F, class... Args>
auto add(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>;
};
原本的add方法变得让人不认识了,解释一下:
template<class F, class… Args>:这是模板函数的模板参数部分。它声明了函数模板add,其中class F表示一个类型参数,class… Args表示一个模板参数包(C++11引入的可变参数模板)。
auto add(F&& f, Args&&… args):这是函数的声明部分。auto表示返回类型会根据函数体内的具体实现而推导得出。F&& f表示一个通用引用(universal reference),用于接收传递给函数的函数对象f。Args&&… args表示一个模板参数包,用于接收传递给函数的参数列表。
-> std::future<typename std::result_of<F(Args…)>::type>:这是函数的返回类型部分。->后面跟着返回类型的声明。std::future是C++标准库中用于异步操作的类模板,它表示一个可能还没有完成的异步操作。typename std::result_of<F(Args…)>::type其中std::result_of用于获取函数调用的返回类型,它接受函数类型和参数类型,并提供一个type成员,表示函数调用的返回类型。在这里,F表示函数类型,Args…表示参数类型,因此std::result_of<F(Args…)>表示函数f的返回类型,而typename std::result_of<F(Args…)>::type表示该返回类型。
由于C++并非不支持模板分离编译(或者单独写一个文件能解决),因此需要在头文件中进行方法实现,首先利用using定义类型别名
template<class F, class... Args>
auto ThreadPool::add(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type> {
using return_type = typename std::result_of<F(Args...)>::type;
auto task = std::make_shared< std::packaged_task<return_type()> >(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<return_type> res = task->get_future();
{
std::unique_lock<std::mutex> lock(tasks_mtx);
// don't allow enqueueing after stopping the pool
if(stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
tasks.emplace([task](){ (*task)(); });
}
cv.notify_one();
return res;
}
创建了一个std::shared_ptr指向一个std::packaged_task对象的智能指针task。packaged_task用于包装可调用对象,并将其结果存储在std::future中,以便在需要时获取。
std::future<return_type> res = task->get_future();: 获取了task中的std::future对象,以便在任务执行完成后获取任务的返回值。
之后就是开始加锁并检查线程池是否停止运行,如果停止运行则抛出异常。
tasks.emplace(task{ (*task)(); });: 将一个 lambda 表达式添加到任务队列tasks中。这个 lambda 表达式捕获了task指针,并调用(*task)(),即执行被包装的任务。
cv.notify_one();: 通过条件变量cv通知一个等待的线程有任务可执行。
return res;: 返回任务的std::future对象,以便调用者可以在需要时获取任务的返回值。
这部分用到了很多C++11中的新特性,代码读起来很困难,能学到很多新特性的用法。
发生变化的add在声明中进行了实现,使得add不再需要手动绑定参数,并获得任务返回值。但是目前项目中调用add还是使用的void EventLoop::addThread(std::function<void()> func)传入无参函数。
11、服务器类
这里对newConnection、deleteConnection进行了重写,加入了保证安全的添加和释放操作。
void Server::newConnection(Socket *sock){
if(sock->getFd() != -1){
Connection *conn = new Connection(loop, sock);
std::function<void(int)> cb = std::bind(&Server::deleteConnection, this, std::placeholders::_1);
conn->setDeleteConnectionCallback(cb);
connections[sock->getFd()] = conn;
}
}
void Server::deleteConnection(int sockfd){
if(sockfd != -1){
auto it = connections.find(sockfd);
if(it != connections.end()){
Connection *conn = connections[sockfd];
connections.erase(sockfd);
delete conn;
}
}
}
测试程序(有利于理解如何使用多线程、高并发)
其中optstring 表示参数项,带冒号指必须需要输入参数。
while ((o = getopt(argc, argv, optstring)) != -1)会循环读取参数中的每一项。
./test -t 1000 -m 10 -w 100
threads 指线程数量;msgs 指消息数量;wait 指等待时长
int main(int argc, char *argv[]) {
int threads = 100;
int msgs = 100;
int wait = 0;
int o;
const char *optstring = "t:m:w:";
while ((o = getopt(argc, argv, optstring)) != -1) {
switch (o) {
case 't':
threads = stoi(optarg);
break;
case 'm':
msgs = stoi(optarg);
break;
case 'w':
wait = stoi(optarg);
break;
case '?':
printf("error optopt: %c\n", optopt);
printf("error opterr: %d\n", opterr);
break;
}
}
ThreadPool *poll = new ThreadPool(threads);
std::function<void()> func = std::bind(oneClient, msgs, wait);
for(int i = 0; i < threads; ++i){
poll->add(func);
}
delete poll;
return 0;
}
之后先创建一个线程池,并将oneClient函数绑定到func并在线程池中进行处理。
oneClient实现如下所示:
和客户端操作相差不大。
void oneClient(int msgs, int wait){
Socket *sock = new Socket();
InetAddress *addr = new InetAddress("127.0.0.1", 1234);
sock->connect(addr);
int sockfd = sock->getFd();
Buffer *sendBuffer = new Buffer();
Buffer *readBuffer = new Buffer();
sleep(wait);
int count = 0;
while(count < msgs){
sendBuffer->setBuf("I'm client!");
ssize_t write_bytes = write(sockfd, sendBuffer->c_str(), sendBuffer->size());
if(write_bytes == -1){
printf("socket already disconnected, can't write any more!\n");
break;
}
int already_read = 0;
char buf[1024]; //这个buf大小无所谓
while(true){
bzero(&buf, sizeof(buf));
ssize_t read_bytes = read(sockfd, buf, sizeof(buf));
if(read_bytes > 0){
readBuffer->append(buf, read_bytes);
already_read += read_bytes;
} else if(read_bytes == 0){ //EOF
printf("server disconnected!\n");
exit(EXIT_SUCCESS);
}
if(already_read >= sendBuffer->size()){
printf("count: %d, message from server: %s\n", count++, readBuffer->c_str());
break;
}
}
readBuffer->clear();
}
delete addr;
delete sock;
}