Analyse du noyau du framework muduo

Analyse du cadre du réseau muduo:

Tout d'abord, nous devons analyser le squelette du coeur qui peut supporter le framework muduo Le squelette du coeur du réacteur moduo est de trois classes, channel, Eventpool et Poller.

Regardez d'abord Channel. Le canal est utilisé pour permettre aux événements de se déplacer librement dans le réacteur. Étant donné que plusieurs threads peuvent partager la mémoire entre chaque thread, la communication entre threads utilise généralement la réplication de mémoire comme principal moyen de communication. Multithreading Muduo Un thread par boucle est utilisé, ce qui signifie que chaque thread a une boucle principale.

Le code source de muduo est simple et facile à comprendre, et la conception est petite. Regardons d'abord le processus d'exécution du code entier à partir de son utilisation. L'utilisation du code muduo:

int main()
{
    //初始化mqtt全局容器
    ::signal(SIGPIPE, SIG_IGN);
    MQTTContainer.globalInit();
    //LOG_INFO << "pid = " << getpid() << ", tid = " << CurrentThread::tid();
    muduo::net::EventLoop loop;
    muduo::net::InetAddress listenAddr(9500);
    DeviceServer::MQTTServer server(&loop, listenAddr);
    server.start();
    loop.loop();
    return 0;
}

Il y a deux classes très fondamentales ici, EventLoop et muduo :: net :: TcpServer. Suivez cet indice et continuez à regarder TcpServer. Que fait commencer? Voyant ici, je vais en fait penser à deux endroits, que fait le constructeur TcpServer ? Qu'a fait le début?

Coder directement

TcpServer::TcpServer(EventLoop* loop,
                     const InetAddress& listenAddr,
                     const string& nameArg,
                     Option option)
  : loop_(CHECK_NOTNULL(loop)),
    ipPort_(listenAddr.toIpPort()),
    name_(nameArg),
    acceptor_(new Acceptor(loop, listenAddr, option == kReusePort)),
    threadPool_(new EventLoopThreadPool(loop, name_)),
    connectionCallback_(defaultConnectionCallback),
    messageCallback_(defaultMessageCallback),
    nextConnId_(1)
{
  acceptor_->setNewConnectionCallback(
      std::bind(&TcpServer::newConnection, this, _1, _2));
}

Les étapes principales

Eventloop initialisé.

loop_(CHECK_NOTNULL(loop))

Pool de threads initialisé

threadPool_(new EventLoopThreadPool(loop, name_))

Récepteur TCP initialisé

acceptor_(new Acceptor(loop, listenAddr, option == kReusePort))

OK, continuons à voir ce que commencera ensuite

void TcpServer::start()
{
  if (started_.getAndSet(1) == 0)
  {
    threadPool_->start(threadInitCallback_);

    assert(!acceptor_->listenning());
    loop_->runInLoop(
        std::bind(&Acceptor::listen, get_pointer(acceptor_)));
  }
}

Après le début, nous avons fait une chose importante, c'est-à-dire que nous avons commencé le pool de threads. Que fait le constructeur de pool de threads? À quoi sert le pool de threads?

Premier regard sur le constructeur de pool de threads

EventLoopThreadPool::EventLoopThreadPool(EventLoop* baseLoop, const string& nameArg)
  : baseLoop_(baseLoop),
    name_(nameArg),
    started_(false),
    numThreads_(0),
    next_(0)
{
}

La fonction principale est d'initialiser le baseLoop, un autre attribut important est le nombre de threads numThreads

C'est bon de continuer à regarder commencer

void EventLoopThreadPool::start(const ThreadInitCallback& cb)
{
  assert(!started_);
  baseLoop_->assertInLoopThread();

  started_ = true;

  for (int i = 0; i < numThreads_; ++i)
  {
    char buf[name_.size() + 32];
    snprintf(buf, sizeof buf, "%s%d", name_.c_str(), i);
    EventLoopThread* t = new EventLoopThread(cb, buf);
    threads_.push_back(std::unique_ptr<EventLoopThread>(t));
    loops_.push_back(t->startLoop());
  }
  if (numThreads_ == 0 && cb)
  {
    cb(baseLoop_);
  }
}

Voir le code est facile à comprendre, muduo a lancé numThreads threads, enregistrant des objets dans les threads du pool de threads dans

Faites attention à ce qu'est cb, cb

const ThreadInitCallback& cb

Est la fonction d'initialisation du thread, qui est l'adresse de rappel de setThreadInitCallback dans muduo

Il y a aussi une opération très fondamentale dans EventLoopThreadPool-> start, EventLoopThread. Nous devons toujours faire attention à ce qu'il initialise et à ce que fait startLoop. Nous pouvons voir que si le nombre de threads est 0,

  if (numThreads_ == 0 && cb)
  {
    cb(baseLoop_);
  }

Il exécutera directement la fonction d'initialisation du thread, sinon, il entrera EventLoopThread lorsqu'il est initialisé. Jetons un coup d'œil au code constructeur d'EventLoopThread.

EventLoopThread::EventLoopThread(const ThreadInitCallback& cb,
                                 const string& name)
  : loop_(NULL),
    exiting_(false),
    thread_(std::bind(&EventLoopThread::threadFunc, this), name),
    mutex_(),
    cond_(mutex_),
    callback_(cb)
{
}

Thread_ mutex_ cond_ callback_ initialisé (rappel d'initialisation de thread), initialisation de certains composants de thread et instances de thread importants, puis nous regardons directement startLoop

EventLoop* EventLoopThread::startLoop()
{
  assert(!thread_.started());
  thread_.start();

  EventLoop* loop = NULL;
  {
    MutexLockGuard lock(mutex_);
    while (loop_ == NULL)
    {
      cond_.wait();
    }
    loop = loop_;
  }

  return loop;
}

Voici vraiment un morceau de code très intéressant. Démarrez le thread au début, puis utilisez la variable de condition pour attendre à l'extérieur jusqu'à ce que EventLoop soit initialisé dans le thread, puis affectez-le à la boucle. Ce code utilise les variables de condition de la glibc très Cela montre que les variables de condition sont un outil puissant dans la programmation multithread. pthread_create est appelé au début, vous ne le croyez pas? Continuez à regarder le code dans Thread-> start!

void Thread::start()
{
  assert(!started_);
  started_ = true;
  // FIXME: move(func_)
  detail::ThreadData* data = new detail::ThreadData(func_, name_, &tid_, &latch_);
  if (pthread_create(&pthreadId_, NULL, &detail::startThread, data))
  {
    started_ = false;
    delete data; // or no delete?
    LOG_SYSFATAL << "Failed in pthread_create";
  }
  else
  {
    latch_.wait();
    assert(tid_ > 0);
  }
}

Une fois le thread créé, le code de démarrage du thread est dans startThread, et nous continuons à réfléchir à ce qui est fait dans startThread? , Je dois dire muduo ici, la classe Thread est conçue avec son ingéniosité

int Thread::join()
{
  assert(started_);
  assert(!joined_);
  joined_ = true;
  return pthread_join(pthreadId_, NULL);
}

Thread::~Thread()
{
  if (started_ && !joined_)
  {
    pthread_detach(pthreadId_);
  }
}

Vous pouvez utiliser join pour la surveillance de la synchronisation. Bien sûr, si vous ne le faites pas, il n'y a pas de problème. Si le destructeur de threads constate que le thread n'est pas fermé lorsque vous détruisez à nouveau le thread, il appellera pthread_detach pour empêcher votre thread La conception n'est-elle pas intelligente?

Eh bien, réfléchissons à ce qui est fait dans startThread!

void* startThread(void* obj)
{
  ThreadData* data = static_cast<ThreadData*>(obj);
  data->runInThread();
  delete data;
  return NULL;
}

En fait appelé dans le fil

 thread_(std::bind(&EventLoopThread::threadFunc, this), name),

threadFunc

void EventLoopThread::threadFunc()
{
  EventLoop loop;

  if (callback_)
  {
    callback_(&loop);
  }

  {
    MutexLockGuard lock(mutex_);
    loop_ = &loop;
    cond_.notify();
  }

  loop.loop();
  //assert(exiting_);
  MutexLockGuard lock(mutex_);
  loop_ = NULL;
}

Déclenchez la fonction d'initialisation du thread, initialisez la boucle et notifiez au thread principal que l'initialisation est terminée cond.notify ();

Une portée très intelligente réduit la portée du verrou, puis démarre la boucle d'événements. Eh bien, nous utilisons une image pour analyser l'ensemble du processus. Notez que le point clé est que l'EventLoop dans chaque thread sera stocké dans le thread après le l'initialisation de chaque thread est terminée. Dans les std :: vector _loops du pool, il est très exposé à TcpServer pour faciliter la communication entre TcpServer et le pool de threads multi-threads

 

 

Que fait-on dans chaque thread du pool de threads et que fait-on dans le Tcpserver? Comment réaliser la communication événementielle entre les threads est la question à se poser!

 

Acceptor a deux attributs importants, Channel et sockfd. Lorsque Acceptor est initialisé, sockfd est donné à acceptChannel_

Acceptor::Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport)
  : loop_(loop),
    acceptSocket_(sockets::createNonblockingOrDie(listenAddr.family())),
    acceptChannel_(loop, acceptSocket_.fd()),
    listenning_(false),
    idleFd_(::open("/dev/null", O_RDONLY | O_CLOEXEC))
{
  assert(idleFd_ >= 0);
  acceptSocket_.setReuseAddr(true);
  acceptSocket_.setReusePort(reuseport);
  acceptSocket_.bindAddress(listenAddr);
  acceptChannel_.setReadCallback(
      std::bind(&Acceptor::handleRead, this));
}

Nous voyons que handleRead est appelé lorsque le pipeline reçoit un événement lisible, regardons à nouveau handleRead

void Acceptor::handleRead()
{
  loop_->assertInLoopThread();
  InetAddress peerAddr;
  //FIXME loop until no more
  int connfd = acceptSocket_.accept(&peerAddr);
  if (connfd >= 0)
  {
    // string hostport = peerAddr.toIpPort();
    // LOG_TRACE << "Accepts of " << hostport;
    if (newConnectionCallback_)
    {
      newConnectionCallback_(connfd, peerAddr);
    }
    else
    {
      sockets::close(connfd);
    }
  }
  else
  {
    LOG_SYSERR << "in Acceptor::handleRead";
    // Read the section named "The special problem of
    // accept()ing when you can't" in libev's doc.
    // By Marc Lehmann, author of libev.
    if (errno == EMFILE)
    {
      ::close(idleFd_);
      idleFd_ = ::accept(acceptSocket_.fd(), NULL, NULL);
      ::close(idleFd_);
      idleFd_ = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
    }
  }
}

Si une nouvelle connexion arrive, appelez newConnectionCallback , qu'est-ce que newConnectionCallback ? Il s'est avéré être un rappel lié dans TcpServer

acceptor_->setNewConnectionCallback(
      std::bind(&TcpServer::newConnection, this, _1, _2));

Regardez de plus près ce que fait newConnection

void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
  loop_->assertInLoopThread();
  EventLoop* ioLoop = threadPool_->getNextLoop();
  char buf[64];
  snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_);
  ++nextConnId_;
  string connName = name_ + buf;

  LOG_INFO << "TcpServer::newConnection [" << name_
           << "] - new connection [" << connName
           << "] from " << peerAddr.toIpPort();
  InetAddress localAddr(sockets::getLocalAddr(sockfd));
  // FIXME poll with zero timeout to double confirm the new connection
  // FIXME use make_shared if necessary
  TcpConnectionPtr conn(new TcpConnection(ioLoop,
                                          connName,
                                          sockfd,
                                          localAddr,
                                          peerAddr));
  connections_[connName] = conn;
  conn->setConnectionCallback(connectionCallback_);
  conn->setMessageCallback(messageCallback_);
  conn->setWriteCompleteCallback(writeCompleteCallback_);
  conn-setCloseCallback(
      std::bind(&TcpServer::removeConnection, this, _1)); // FIXME: unsafe
  ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));
}

Générez abstraitement un TcpConnectionPtr, puis appelez runInLoop, continuez à observer le code runInLoop

void EventLoop::runInLoop(Functor cb)
{
  if (isInLoopThread())
  {
    cb();
  }
  else
  {
    queueInLoop(std::move(cb));
  }
}

Continuer à afficher la file d'attente

void EventLoop::queueInLoop(Functor cb)
{
  {
  MutexLockGuard lock(mutex_);
  pendingFunctors_.push_back(std::move(cb));
  }

  if (!isInLoopThread() || callingPendingFunctors_)
  {
    wakeup();
  }
}

Suspendez la fonction à démarrer sur pendingFunctors_, puis appelez wakeup pour réveiller le thread correspondant

void EventLoop::wakeup()
{
  uint64_t one = 1;
  ssize_t n = sockets::write(wakeupFd_, &one, sizeof one);
  if (n != sizeof one)
  {
    LOG_ERROR << "EventLoop::wakeup() writes " << n << " bytes instead of 8";
  }
}

Réfléchissez à la façon de trouver le thread correspondant lorsque la connexion se produit, voir ici

EventLoop* ioLoop = threadPool_->getNextLoop();

Combiné avec l'initialisation d'EventPool

EventLoop::EventLoop()
  : looping_(false),
    quit_(false),
    eventHandling_(false),
    callingPendingFunctors_(false),
    iteration_(0),
    threadId_(CurrentThread::tid()),
    poller_(Poller::newDefaultPoller(this)),
    timerQueue_(new TimerQueue(this)),
    wakeupFd_(createEventfd()),
    wakeupChannel_(new Channel(this, wakeupFd_)),
    currentActiveChannel_(NULL)
{
  LOG_DEBUG << "EventLoop created " << this << " in thread " << threadId_;
  if (t_loopInThisThread)
  {
    LOG_FATAL << "Another EventLoop " << t_loopInThisThread
              << " exists in this thread " << threadId_;
  }
  else
  {
    t_loopInThisThread = this;
  }
  wakeupChannel_->setReadCallback(
      std::bind(&EventLoop::handleRead, this));
  // we are always reading the wakeupfd
  wakeupChannel_->enableReading();
}

Eh bien, c'est fondamentalement clair, et enfin utiliser un diagramme de synthèse

 

 

Enfin, le réveil mutuel entre les canaux se fait via eventfd, et chaque canal utilise wakefd dans Eventloop, qui est eventfd pour se réveiller mutuellement.

Je suppose que tu aimes

Origine blog.csdn.net/qq_32783703/article/details/108014372
conseillé
Classement