muduo网络库剖析——事件循环与线程EventLoopThread接口类
- 前情
- 从muduo到my_muduo
- 概要
- bind
- unique_lock< mutex > 与 condition_variable
- 框架与细节
- 成员
- 函数
- 使用方法
- 源码
- 结尾
前情
从muduo到my_muduo
作为一个宏大的、功能健全的muduo库,考虑的肯定是众多情况是否可以高效满足;而作为学习者,我们需要抽取其中的精华进行简要实现,这要求我们足够了解muduo库。
做项目 = 模仿 + 修改,不要担心自己学了也不会写怎么办,重要的是积累,学到了这些方法,如果下次在遇到通用需求的时候你能够回想起之前的解决方法就够了。送上一段话!
概要
因为EventLoop与Thread是两个独立的类,如果要将两个类产生关联,实现one loop one thread,还需要一个EventLoop和Thread的接口类,即EventLoopThread类,其中会实现EventLoop与Thread的启动与关闭,一个EventLoopThread即对应着一个one loop one thread。
bind
里面用到了bind函数,这里通过学习ZYH665的博客,了解到bind的多种用法。
对于bind函数。bind共可以绑定四种函数,分别为 普通函数,类成员函数,类静态函数,lambda表达式,下面将详细介绍四种函数的使用方法。
首先是普通函数中的无参函数。使用方法如下:
接着是有参普通函数,对于有参数的函数,可以选择绑定哪些参数,参数的顺序需要顺序连续,若是有的参数不需要绑定,就要使用 std::placeholders::_1 占位标识符。使用方式如下:
占位符按如下方式变化:
auto ite = std::bind(tes,placeholders::_4, placeholders::_3, placeholders::_4, placeholders::_4);
ite(100,200,300,400);
auto ite2 = std::bind(tes, 100, placeholders::_2, 300, placeholders::_1);
ite2(200, 400);
接着是类成员函数,此时bind至少需要两个参数,第一个参数为类的成员函数(需要通过 &类名::成员函数 的方式填入参数,第二个参数为类对象或者类对象的this指针,对象地址也可。
bind还可以绑定类静态函数,对于类的静态函数就不用传递第二个参数了,直接通过 &类名::成员函数 的方式填入第一个参数即可。
除此以外还可以绑定lamdba函数。
unique_lock< mutex > 与 condition_variable
通过阅读༄yi笑奈何的博客,我更加了解condition_variable的使用方式。
条件变量std::condition_variable的作用是阻塞线程,然后等待通知将其唤醒。我们可以通过某个函数判断是否符合某种条件来决定是阻塞线程等待通知还是唤醒线程,由此实现线程间的同步。所以简单来说condition_variable的作用就两个——等待(wait)、通知(notify)。下面详细介绍一下。
首先是wait函数。wait()的是普通的等待。分为有条件的等待和无条件的等待。
void wait (unique_lock& lck)会无条件的阻塞当前线程然后等待通知,前提是此时对象lck已经成功获取了锁。等待时会调用lck.unlock()释放锁,使其它线程可以获取锁。一旦得到通知(由其他线程显式地通知),函数就会释放阻塞并调用lck.lock(),使lck保持与调用函数时相同的状态。然后函数返回,注意,最后一次lck.lock()的调用可能会在返回前再次阻塞线程。
使用方法是:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <windows.h>
std::condition_variable cv;
int value;
void read_value() {
std::cin >> value;
cv.notify_one();
}
int main()
{
std::cout << "Please, enter an integer (I'll be printing dots): \n";
std::thread th(read_value);
std::mutex mtx;
std::unique_lock<std::mutex> lck(mtx);
cv.wait(lck);
std::cout << "You entered: " << value << '\n';
th.join();
return 0;
}
wait()函数因为没有条件判断,因此有时候会产生虚假唤醒,而有条件的等待可以很好的解决这一问题。
void wait (unique_lock& lck, Predicate pred)为有条件的等待,pred是一个可调用的对象或函数,它不接受任何参数,并返回一个可以作为bool计算的值。当pred为false时wait()函数才会使线程等待,在收到其他线程通知时只有当pred返回true时才会被唤醒。我们将上述代码做如下修改,它的输出与上述一样。
使用方式如下:
bool istrue()
{
return value != 0;
}
int main()
{
std::cout << "Please, enter an integer (I'll be printing dots): \n";
std::thread th(read_value);
std::mutex mtx;
std::unique_lock<std::mutex> lck(mtx);
cv.wait(lck,istrue);
std::cout << "You entered: " << value << '\n';
th.join();
return 0;
}
框架与细节
成员
EventLoop成员中有一个loop,以及一个thread,以及thread中函数的回调。再包括用来支持线程安全性的互斥锁与条件变量。
函数
首先,肯定是EventLoopThread的构造函数。构造函数对该类中的loop,thread,线程初始化函数回调等成员进行了初始化。函数回调默认的是传入一个空函数,默认的thread名也是空字符串。对于函数回调的传入,主要使用了c++11的绑定器,如果是类中函数,还需要传第二个参数this指针。
接着是析构函数,其中会将退出状态位置1,且将对应的loop会quit,thread也会进行线程分离join。
startLoop函数里面主要是启动了thread_,调用thread_的start函数,此时应该与loop_做mapping。此时loop_还为空,loop_的赋值在之后的threadFunc中,通过unique_lock< mutex >与condition_variable的结合,会一直等待loop_被赋值了。最终会返回loop,告诉调用者这个EventLoopThread中的loop是哪个。
对于threadFunc这个函数,用于处理创建相应loop并执行事件循环。我在剖析时产生了一个问题,就是为什么loop_明明是one loop one thread,一个loop对应一个线程,为什么会产生线程安全的问题的呢?为什么需要用到mutex之类的解决线程安全问题的变量呢?在我的印象中,像webserver中的请求队列,是对多个工作线程开放的,这种是需要处理线程安全问题的。
其实,对于loop_,startLoop的主线程会访问loop_,另外EventLoopThread中的thread在调用threadFunc也会访问loop_。所以,也会存在线程安全问题的。
另外,在创建loop前,还需要对thread进行初始化,使用的ThreadInitCallBack回调函数,这个函数是提供给其他类的一个接口,实现方式在其他类中,这里就不再赘述。
使用方法
源码
//EventLoopThread.h
#pragma once
#include "noncopyable.h"
#include "Thread.h"
#include <functional>
#include <mutex>
#include <condition_variable>
#include <string>
class EventLoop;
class EventLoopThread : noncopyable {
public:
using ThreadInitCallback = std::function<void(EventLoop*)>;
EventLoopThread(const ThreadInitCallback &cb = ThreadInitCallback(), const std::string &name = std::string()); //空函数或者用于初始化某些全局状态的函数
~EventLoopThread();
EventLoop* startLoop();
private:
void threadFunc();
EventLoop *loop_;
bool exiting_;
Thread thread_;
std::mutex mutex_;
std::condition_variable cond_;
ThreadInitCallback callback_;
};
//EventLoopThread.cc
#include "EventLoopThread.h"
#include "EventLoop.h"
EventLoopThread::EventLoopThread(const ThreadInitCallback &cb,
const std::string &name)
: loop_(nullptr)
, exiting_(false)
, thread_(std::bind(&EventLoopThread::threadFunc, this), name)
, mutex_()
, cond_()
, callback_(cb) {
}
EventLoopThread::~EventLoopThread() {
exiting_ = true;
if (loop_ != nullptr) {
loop_->quit();
thread_.join();
}
}
EventLoop* EventLoopThread::startLoop() {
thread_.start(); // 启动底层的新线程
EventLoop *loop = nullptr;
{
std::unique_lock<std::mutex> lock(mutex_);
while ( loop_ == nullptr ) {
cond_.wait(lock);
}
loop = loop_;
}
return loop;
}
// 下面这个方法,实在单独的新线程里面运行的
void EventLoopThread::threadFunc() {
EventLoop loop; // 创建一个独立的eventloop,和上面的线程是一一对应的,one loop per thread
if (callback_) {
callback_(&loop);
}
{
std::unique_lock<std::mutex> lock(mutex_);
loop_ = &loop;
cond_.notify_one();
}
loop.loop(); // EventLoop loop => Poller.poll
std::unique_lock<std::mutex> lock(mutex_);
loop_ = nullptr;
}
结尾
以上就是事件循环与线程EventLoopThread接口类的相关介绍,以及我在进行项目重写的时候遇到的一些问题,和我自己的一些心得体会。发现写博客真的会记录好多你的成长,而且对于一个好的项目,写博客也是证明你确实有过深度思考,并且在之后面试或者工作时遇到同样的问题能够进行复盘的一种有效的手段。所以,希望uu们也可以像我一样,养成写博客的习惯,逐渐脱离菜鸡队列,向大佬前进!!!加油!!!
也希望我能够完成muduo网络库项目的深度学习与重写,并在功能上能够拓展。也希望在完成这个博客系列之后,能够引导想要学习muduo网络库源码的人,更好地探索这篇美丽繁华的土壤。致敬chenshuo大神!!!
鉴于博主只是一名平平无奇的大三学生,没什么项目经验,所以可能很多东西有所疏漏,如果有大神发现了,还劳烦您在评论区留言,我会努力尝试解决问题!