muduo网络库浅谈(一)

2023-11-02

序言

C++的学习过程充满着迷茫,C++primer,侯捷老师的STL源码剖析,再到boost库,C++多线程库,纷繁复杂的数据结构,以及为了效率无所不用其极的函数重载,shared_ptr的线程安全性,多线程访问时数据竞争,C++对于C的进步在于安全的内存管理,以及大量库支持,避免了我们重复造轮子的过程。最近看了陈硕老师的muduo网络库,该网络库是一个C++网络服务中的经典,muduo库的意义在于:“奋木铎以警众,使明听也”,为了不至于看久了忘记,就撰写这篇博文,本博客将会从程硕老师的出版图书《Linux多线程服务端编程 使用muduo C++网络库》的第八章开始讲解(某东上打折很给力,买来看看还是很值得的!希望大家支持正版)。

在正式介绍之前不妨先思考一下网络库的设计需求,设立几个小目标。
1.一个单线程的事件处理机制。
2.一个基于网络通信的事件处理(举例来说,对通信时间进行处理以及处理后发送的处理机制)。
3.多线程下实现上述1,2要求。

接下来将分为三个章节来讲述要求1,2,3,的源码解读。

第一章 muduo的关键结构

class EventLoop

muduo网络库将网络服务通信的各个流程中的环节封装成不同的class,高度模块化的设计使得以后拓展接口提供了极大地便利,class EventLoop是网络库的重要组成部分,初始EventLoop什么也不做,代码如下:

#ifndef MUDUO_NET_EVENTLOOP_H
#define MUDUO_NET_EVENTLOOP_H

#include "thread/Thread.h"

namespace muduo
{

class EventLoop : boost::noncopyable
{
 public:

  EventLoop();
  ~EventLoop();

  void loop();

  void assertInLoopThread()
  {
    if (!isInLoopThread())
    {
      abortNotInLoopThread();
    }
  }

  bool isInLoopThread() const { return threadId_ == CurrentThread::tid(); }

 private:

  void abortNotInLoopThread();

  bool looping_; 
  const pid_t threadId_;
};

}

#endif 

可以看到EventLoop定义有构造,析构,loop,isInLoopThread,abortNotInLoopThread等函数,成员函数的定义如下:

#include "EventLoop.h"

#include "logging/Logging.h"

#include <assert.h>
#include <poll.h>

using namespace muduo;

__thread EventLoop* t_loopInThisThread = 0;//thread_local变量

EventLoop::EventLoop()
  : looping_(false),
    threadId_(CurrentThread::tid())
{
  LOG_TRACE << "EventLoop created " << this << " in thread " << threadId_;
  if (t_loopInThisThread)
  {
    LOG_FATAL << "Another EventLoop " << t_loopInThisThread
              << " exists in this thread " << threadId_;
  }
  else
  {
    t_loopInThisThread = this;
  }
}

EventLoop::~EventLoop()
{
  assert(!looping_);
  t_loopInThisThread = NULL;
}

void EventLoop::loop()
{
  assert(!looping_);
  assertInLoopThread();
  looping_ = true;

  ::poll(NULL, 0, 5*1000);

  LOG_TRACE << "EventLoop " << this << " stop looping";
  looping_ = false;
}

void EventLoop::abortNotInLoopThread()
{
  LOG_FATAL << "EventLoop::abortNotInLoopThread - EventLoop " << this
            << " was created in threadId_ = " << threadId_
            << ", current thread id = " <<  CurrentThread::tid();
}

其中t_loopInThisThread属于thread变量,存储当前线程的EventLoop指针,初始为0,在构造函数中,对事件循环的looping_标志位,以及线程号进行初始化(一个EventLoop对应一个线程,避免多线程下的数据竞争以及访问逻辑混乱的情况,所以使用大量assertInLoopThread以及boost:noncopyable以保证EventLoop在本线程中运行),LOG_TRACE 等属于日志,便于意外情况下对服务器运行情况的分析,以后不再赘述。

析构函数太过简单不需要解释;loop()函数用于正式启用事件循环,本例中loop()并不完全,poll函数属于IO复用,之后会讲解。abortNotInLoopThread()将线程运行错误输入日志。以上便是一个EventLoop的基本结构,但并不涉及具体的事件处理机制。

class Channel

在介绍class Channel之前,不得不说说Linux下IO复用——poll函数。

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

pollfd为poll函数需要监听的事件,nfds为需要监听事件的数量,pollfd

struct pollfd{
	int fd;			//文件描述符
	short events;	//需要监听的事件
	short revents;	//实际发生的事件
};

Linux一大特色便是万物皆文件,int fd指定了一个文件描述符,poll函数会根据events(用户设置)来监听fd,并根据发生的事件重写revents,并返回具体事件发生的数量。

因此对于上述调用poll环节可以分为class Channelclass Poller两部分!以下为class Channel的源码:

#ifndef MUDUO_NET_CHANNEL_H
#define MUDUO_NET_CHANNEL_H

#include <boost/function.hpp>
#include <boost/noncopyable.hpp>

namespace muduo
{

class EventLoop;
class Channel : boost::noncopyable
{
 public:
  typedef boost::function<void()> EventCallback;//定义回调函数

  Channel(EventLoop* loop, int fd);

  void handleEvent();//事件具体处理
  void setReadCallback(const EventCallback& cb)//读回调函数设置
  { readCallback_ = cb; }
  void setWriteCallback(const EventCallback& cb)//写回调函数设置
  { writeCallback_ = cb; }
  void setErrorCallback(const EventCallback& cb)//错误回调函数设置
  { errorCallback_ = cb; }

  int fd() const { return fd_; }
  int events() const { return events_; }
  void set_revents(int revt) { revents_ = revt; }
  bool isNoneEvent() const { return events_ == kNoneEvent; }

  void enableReading() { events_ |= kReadEvent; update(); }//该函数负责向poller的channal列表进行注册
  // void enableWriting() { events_ |= kWriteEvent; update(); }
  // void disableWriting() { events_ &= ~kWriteEvent; update(); }
  // void disableAll() { events_ = kNoneEvent; update(); }

  // for Poller
  int index() { return index_; }
  void set_index(int idx) { index_ = idx; }

  EventLoop* ownerLoop() { return loop_; }

 private:
  void update();

  static const int kNoneEvent;//
  static const int kReadEvent;//
  static const int kWriteEvent;//

  EventLoop* loop_;//所属的EventLoop
  const int  fd_;
  int        events_;
  int        revents_;
  int        index_; // used by Poller.

  EventCallback readCallback_;//函数指针
  EventCallback writeCallback_;//函数指针
  EventCallback errorCallback_;//函数指针
};

成员函数的实现:

#include "Channel.h"
#include "EventLoop.h"
#include "logging/Logging.h"

#include <sstream>

#include <poll.h>

using namespace muduo;

const int Channel::kNoneEvent = 0;
const int Channel::kReadEvent = POLLIN | POLLPRI;
const int Channel::kWriteEvent = POLLOUT;

Channel::Channel(EventLoop* loop, int fdArg)
  : loop_(loop),
    fd_(fdArg),
    events_(0),
    revents_(0),
    index_(-1)
{
}

void Channel::update()
{
  loop_->updateChannel(this);
}

void Channel::handleEvent()//根据poll返回的事件revents_标识符调用相应的事件回调函数
{
  if (revents_ & POLLNVAL) {
    LOG_WARN << "Channel::handle_event() POLLNVAL";
  }

  if (revents_ & (POLLERR | POLLNVAL)) {
    if (errorCallback_) errorCallback_();
  }
  if (revents_ & (POLLIN | POLLPRI | POLLRDHUP)) {
    if (readCallback_) readCallback_();
  }
  if (revents_ & POLLOUT) {
    if (writeCallback_) writeCallback_();
  }
}

可以看出,class Channel的功能主要在于创建事件pollfd以及相应的回调函数(一个需要监听的事件pollfd对应一个Channel,一个Channel也唯一属于一个EventLoop,相同的事件不需要在不同的EventLoop中重复的监听,逻辑上是没必要的,因此一个事件的Channel具有唯一性,boost::noncopyable),唯一没有介绍的是 enableReading()以及int index,enableReading()代表一个Channel实例已经可读,并调用自身的私有函数update(),继而调用EventLoop的updateChannel()成员函数向EventLoop注册自己,表示自己需要加入到poll的监听队列(此处的队列是一种说法,并非数据结构)中,index用于表明在监听队列中的编号

那么问题来了,具体内部注册具有是怎么实现的呢?不妨将这个问题设为问题1

根据上述问题,EventLoop则需要改变一下,增加新的成员函数,新的代码如下:

#ifndef MUDUO_NET_EVENTLOOP_H
#define MUDUO_NET_EVENTLOOP_H

#include "thread/Thread.h"

#include <boost/scoped_ptr.hpp>
#include <vector>

namespace muduo
{

class Channel;
class Poller;

class EventLoop : boost::noncopyable
{
 public:

  EventLoop();

  // force out-line dtor, for scoped_ptr members.
  ~EventLoop();

  ///
  /// Loops forever.
  ///
  /// Must be called in the same thread as creation of the object.
  ///
  void loop();

  void quit();//新增

  // internal use only
  void updateChannel(Channel* channel);//新增
  // void removeChannel(Channel* channel);

  void assertInLoopThread()
  {
    if (!isInLoopThread())
    {
      abortNotInLoopThread();
    }
  }

  bool isInLoopThread() const { return threadId_ == CurrentThread::tid(); }

 private:

  void abortNotInLoopThread();

  typedef std::vector<Channel*> ChannelList;//定义保存数据的结构

  bool looping_; //循环标志位,涉及loop()中的逻辑,使得loop()无法重复运行。
  bool quit_; //停止标志位
  const pid_t threadId_;
  boost::scoped_ptr<Poller> poller_;//拥有一个class Poller的实例
  ChannelList activeChannels_;//活动事件的列表
};

}

#endif 

成员函数如下:

#include "EventLoop.h"

#include "Channel.h"
#include "Poller.h"

#include "logging/Logging.h"

#include <assert.h>

using namespace muduo;

__thread EventLoop* t_loopInThisThread = 0;
const int kPollTimeMs = 10000;

EventLoop::EventLoop()
  : looping_(false),
    quit_(false),
    threadId_(CurrentThread::tid()),
    poller_(new Poller(this))
{
  LOG_TRACE << "EventLoop created " << this << " in thread " << threadId_;
  if (t_loopInThisThread)
  {
    LOG_FATAL << "Another EventLoop " << t_loopInThisThread
              << " exists in this thread " << threadId_;
  }
  else
  {
    t_loopInThisThread = this;
  }
}

EventLoop::~EventLoop()
{
  assert(!looping_);
  t_loopInThisThread = NULL;
}

void EventLoop::loop()//loop()函数正式增加了poll监听事件的结构
{
  assert(!looping_);
  assertInLoopThread();
  looping_ = true;
  quit_ = false;

  while (!quit_)
  {
    activeChannels_.clear();//上一次的活跃Channel删除;
    poller_->poll(kPollTimeMs, &activeChannels_);//调用Class Poller的成员函数,返回活跃的Channel数组
    for (ChannelList::iterator it = activeChannels_.begin();
        it != activeChannels_.end(); ++it)
    {
      (*it)->handleEvent();//调用Channel对应的回调函数
    }
  }

  LOG_TRACE << "EventLoop " << this << " stop looping";
  looping_ = false;
}

void EventLoop::quit()
{
  quit_ = true;
  // wakeup();
}

void EventLoop::updateChannel(Channel* channel)
{
  assert(channel->ownerLoop() == this);
  assertInLoopThread();
  poller_->updateChannel(channel);//注册一个Channel的工作转移到了class Poller
}

void EventLoop::abortNotInLoopThread()
{
  LOG_FATAL << "EventLoop::abortNotInLoopThread - EventLoop " << this
            << " was created in threadId_ = " << threadId_
            << ", current thread id = " <<  CurrentThread::tid();
}


以上的代码并没有增加太多的成员函数,但也没有解释问题1——Channel是怎么注册的(EventLoop把问题推给了Class Poller),同时在loop()函数中调用了Class Poller的成员函数,私有成员中增加boost::scoped_ptr poller_(这很好理解,Poller内部封装由poll的功能,EventLoop独自拥有一个Class Poller的实例就可以实现监听所有的Channel),至此,所有的问题都推给了Class Poller,代码如下:

class Poller

#ifndef MUDUO_NET_POLLER_H
#define MUDUO_NET_POLLER_H

#include <map>
#include <vector>

#include "datetime/Timestamp.h"
#include "EventLoop.h"

struct pollfd;

namespace muduo
{

class Channel;
class Poller : boost::noncopyable
{
 public:
  typedef std::vector<Channel*> ChannelList;

  Poller(EventLoop* loop);
  ~Poller();
  Timestamp poll(int timeoutMs, ChannelList* activeChannels);//poll成员函数,与上文的poll函数不一样。
  void updateChannel(Channel* channel);//向Class Poller注册Channel;

  void assertInLoopThread() { ownerLoop_->assertInLoopThread(); }//emmm,不必多说

 private:
  void fillActiveChannels(int numEvents,
                          ChannelList* activeChannels) const;//填充活跃的Channel

  typedef std::vector<struct pollfd> PollFdList;//定义了存储pollfd的结构,注意为vector。
  typedef std::map<int, Channel*> ChannelMap;//Channel的存储结构,Channel并不直接存储pollfd,而是存储struct pollfd中对应的成员,map<int, Channel*>中int为pollfd->fd,也就是时class Channel的成员fd_。

  EventLoop* ownerLoop_;//
  PollFdList pollfds_;//
  ChannelMap channels_;//
};

}
#endif

上述中pollfds_为vector,用于高效地随机访问(Channel中保存了在vector中的下标index),channels_使用map来管理,实现高效地查找,删除和插入Channel(尽管当前没有实现删除)。

再看看成员函数的实现:

#include "Poller.h"

#include "Channel.h"
#include "logging/Logging.h"

#include <assert.h>
#include <poll.h>

using namespace muduo;

Poller::Poller(EventLoop* loop)
  : ownerLoop_(loop)//保存有所属的EventLoop指针
{
}

Poller::~Poller()//析构函数什么也不做,不要惊讶,其成员都是stl中的容器,容器内保存的是一个类的实例则自动调用其析构,具体可参考《stl源码剖析》的2.2.3小节;
//如果为指针,需要手动管理析构,但Channel的创建和析构都不属于class Poller的工作范畴,具体可参考下文class TimerQueue的析构。
{
}

Timestamp Poller::poll(int timeoutMs, ChannelList* activeChannels)
{
 
  int numEvents = ::poll(&*pollfds_.begin(), pollfds_.size(), timeoutMs);//监听pollfds_的文件描述符,并返回发生事件
  Timestamp now(Timestamp::now());//时间戳,之后会介绍
  if (numEvents > 0)//有事件发生
   {
    LOG_TRACE << numEvents << " events happended";
    fillActiveChannels(numEvents, activeChannels);//调用该函数,将活跃的Channel填充至activeChannels中
  } else if (numEvents == 0) {
    LOG_TRACE << " nothing happended";
  } else {
    LOG_SYSERR << "Poller::poll()";
  }
  return now;//返回当前时间戳
}

void Poller::fillActiveChannels(int numEvents,
                                ChannelList* activeChannels) const//填充活跃的pollfd对应的Channel添加至activeChannels中
{
  for (PollFdList::const_iterator pfd = pollfds_.begin();
      pfd != pollfds_.end() && numEvents > 0; ++pfd)//历遍寻找活跃的pollfd
  {
    if (pfd->revents > 0)
    {
      --numEvents;//numEvents活跃事件数量全部找到则可提前结束历遍。
      ChannelMap::const_iterator ch = channels_.find(pfd->fd);//
      assert(ch != channels_.end());
      Channel* channel = ch->second;
      assert(channel->fd() == pfd->fd);
      channel->set_revents(pfd->revents);//填充fd的revents
      // pfd->revents = 0;
      activeChannels->push_back(channel);//填充活跃的Channel至activeChannels数组中。
    }
  }
}

void Poller::updateChannel(Channel* channel)//添加或删除Channel
{
  assertInLoopThread();
  LOG_TRACE << "fd = " << channel->fd() << " events = " << channel->events();
  if (channel->index() < 0)//Channel的index默认为-1,代表未使用过
   {
    // a new one, add to pollfds_
    assert(channels_.find(channel->fd()) == channels_.end());
    struct pollfd pfd;//定义结构体pollfd,将Channel的fd,events赋予pfd中
    pfd.fd = channel->fd();
    pfd.events = static_cast<short>(channel->events());
    pfd.revents = 0;
    pollfds_.push_back(pfd);//pfd将入需要监听的pollfds_数组中。
    int idx = static_cast<int>(pollfds_.size())-1;//根据在pollfds_数组的位置,设置Channel的index,此时index由-1变为pollfds_.size()-1,代表已经添加过。
    channel->set_index(idx);
    channels_[pfd.fd] = channel;//将入map中,可以认为是map<fd,Channel*>
  } else {
    // 该Channel已经被添加过一次,则实现对应的Channel更新
    assert(channels_.find(channel->fd()) != channels_.end());
    assert(channels_[channel->fd()] == channel);
    int idx = channel->index();//得到Channel的index,即在pollfds_中的位置
    assert(0 <= idx && idx < static_cast<int>(pollfds_.size()));
    struct pollfd& pfd = pollfds_[idx];
    assert(pfd.fd == channel->fd() || pfd.fd == -1);
    pfd.events = static_cast<short>(channel->events());//更新events
    pfd.revents = 0;
    if (channel->isNoneEvent()) {
      // ignore this pollfd
      pfd.fd = -1;
    }
  }
}

class Poller的主要作用是维护EventLoop所拥有的的Channel,其成员函数poll用于事件的监听,并返回活跃事件,以及updateChannel()函数向Poller中添加Channel,不过当前的class Poller不具有移除Channel的作用,只能不断的向pollfds_和channels_添加。

以下为三个class的关键函数loop()和enableReading()的调用循序:

主要函数调用顺序

番外

为了给EventLoop根据一定周期执行某个函数,设计出响应的定时器功能,定时器由class TimerId,Timer,TimerQueue,三个来实现的,接下来将一一介绍。

定时任务

首先需要明确两点:1、定时器的作用,无论EventLoop由多少个定时任务,仅需要一个class就可以集中的管理;2、poll是阻塞的,一旦监听的文件描述符没有发生事件,则会阻塞,那么定时任务无法得到及时的执行,所以需要将计时任务作为一个监听事件(也就是Channel)来实现定时的唤醒,计算有哪些计时器到期,执行对应的函数。这样我们就明确了class TimerQueue的功能了!
但在介绍class TimerQueue之前,需要先介绍几个class以便更好地理解定时器的实现:

class Timestamp

class Timestamp的主要作用是保存时间戳(int64_t microSecondsSinceEpoch_;),以及重载了一些运算符,提供了一些如toString(),now()等接口,具体的成员函数的实现不讲了,简单讲一下头文件中成员的作用。

#ifndef MUDUO_BASE_TIMESTAMP_H
#define MUDUO_BASE_TIMESTAMP_H

#include "copyable.h"

#include <stdint.h>
#include <string>

namespace muduo
{
class Timestamp : public muduo::copyable
{
 public:
  Timestamp();//默认构造函数,时间戳设为0
  explicit Timestamp(int64_t microSecondsSinceEpoch);//初始化时间戳为microSecondsSinceEpoch

  void swap(Timestamp& that)//交换时间戳
  {
    std::swap(microSecondsSinceEpoch_, that.microSecondsSinceEpoch_);
  }
  std::string toString() const;//时间戳转为str
  std::string toFormattedString() const;//同上类似
  bool valid() const { return microSecondsSinceEpoch_ > 0; }//时间戳是否可用
  int64_t microSecondsSinceEpoch() const { return microSecondsSinceEpoch_; }//返回时间戳
  static Timestamp now();//静态函数,返回当前时间的时间戳
  static Timestamp invalid();//返回一个为0的时间戳

  static const int kMicroSecondsPerSecond = 1000 * 1000;

 private:
  int64_t microSecondsSinceEpoch_;//时间戳以微妙来存储
};

inline bool operator<(Timestamp lhs, Timestamp rhs)//重载运算符
{
  return lhs.microSecondsSinceEpoch() < rhs.microSecondsSinceEpoch();
}

inline bool operator==(Timestamp lhs, Timestamp rhs)
{
  return lhs.microSecondsSinceEpoch() == rhs.microSecondsSinceEpoch();
}

inline double timeDifference(Timestamp high, Timestamp low)//时间差
{
  int64_t diff = high.microSecondsSinceEpoch() - low.microSecondsSinceEpoch();
  return static_cast<double>(diff) / Timestamp::kMicroSecondsPerSecond;
}
inline Timestamp addTime(Timestamp timestamp, double seconds)//为给定的时间戳增加seconds秒,并返回增加后的时间戳
{
  int64_t delta = static_cast<int64_t>(seconds * Timestamp::kMicroSecondsPerSecond);
  return Timestamp(timestamp.microSecondsSinceEpoch() + delta);
}
}
#endif 

class Timer

上小节class Timestamp构成class Timer的成员,class Timer顾名思义:定时器!首先来明确定时器的作用,EventLoop可以拥有数个定时任务,那么单个定时器不仅需要保存时间戳,定时时间,是否循环,以及相应的时间回调函数。一下为class Timer的头文件:


#ifndef MUDUO_NET_TIMER_H
#define MUDUO_NET_TIMER_H

#include <boost/noncopyable.hpp>

#include "datetime/Timestamp.h"
#include "Callbacks.h"

namespace muduo
{
class Timer : boost::noncopyable//不可拷贝
{
 public:
  Timer(const TimerCallback& cb, Timestamp when, double interval)//构造函数,分别设定回调函数,时间戳,循环间隔,是否重复
    : callback_(cb),
      expiration_(when),
      interval_(interval),
      repeat_(interval > 0.0)
  { }

  void run() const//运行回调函数
  {
    callback_();
  }

  Timestamp expiration() const  { return expiration_; }//返回时间戳
  bool repeat() const { return repeat_; }//返回是否重复

  void restart(Timestamp now);//重启
{
  if (repeat_)
  {
    expiration_ = addTime(now, interval_);//默认=调用
  }
  else
  {
    expiration_ = Timestamp::invalid();//默认=调用
  }
}
 private:
  const TimerCallback callback_;
  Timestamp expiration_;
  const double interval_;
  const bool repeat_;
};

}
#endif  

class TimerQueue

接下来就是class TimerQueue的正体了,class TimerQueue为定时器队列,先来明确class TimerQueue的实现任务:1、提供对外接口——增加定时任务;2、创建一个timerfd(LInux新增了timerfd作为定时任务,用法和上文的pollfd一样,当超时后发生可读事件,所以同样需要Channel,暂且把这个Channel叫他timerqueue Channel,timerqueue Channel对应一个回调函数),并使用poll来监听,对该文件描述符可读事件进行超时通知,从poll返回后调用timerqueue Channel的回调函数,该回调函数会检查class TimerQueue所有定时器是否到期,调用其到期的Timer的回调函数。上代码直接看:

#ifndef MUDUO_NET_TIMERQUEUE_H
#define MUDUO_NET_TIMERQUEUE_H

#include <set>
#include <vector>

#include <boost/noncopyable.hpp>

#include "datetime/Timestamp.h"
#include "thread/Mutex.h"
#include "Callbacks.h"
#include "Channel.h"

namespace muduo
{

class EventLoop;
class Timer;
class TimerId;
class TimerQueue : boost::noncopyable//一个EventLoop对应一个定时器队列
{
 public:
  TimerQueue(EventLoop* loop);
  ~TimerQueue();
  TimerId addTimer(const TimerCallback& cb,
                   Timestamp when,
                   double interval);//对外接口,增加定时器,以及设定相关的Timer的回调函数和定时间隔。
 private:
  typedef std::pair<Timestamp, Timer*> Entry;
  typedef std::set<Entry> TimerList;//set内置红黑树,且自动排序,方便找到已经到期的Timer

  void handleRead();//TimerQueue的回调函数,poll返回后运行,检查其到期的定时器,并运行Timer的回调函数
  std::vector<Entry> getExpired(Timestamp now);//以数组形式返回到期的Timer,
  void reset(const std::vector<Entry>& expired, Timestamp now);//重新设置定时器Timer的时间戳,

  bool insert(Timer* timer);//插入定时器Timer

  EventLoop* loop_;//所属的EventLoop
  const int timerfd_;//timefd的文件描述符
  Channel timerfdChannel_;//timefd对应的Channel实例
  TimerList timers_;//保存定时器Timer
};

}
#endif  // MUDUO_NET_TIMERQUEUE_H

成员函数定义如下:

#define __STDC_LIMIT_MACROS
#include "TimerQueue.h"

#include "logging/Logging.h"
#include "EventLoop.h"
#include "Timer.h"
#include "TimerId.h"

#include <boost/bind.hpp>

#include <sys/timerfd.h>

namespace muduo
{
namespace detail
{

int createTimerfd()//非成员函数,创建timerfd文件描述符用于poll监听,注意没有设置超时时间
{
  int timerfd = ::timerfd_create(CLOCK_MONOTONIC,
                                 TFD_NONBLOCK | TFD_CLOEXEC);
  if (timerfd < 0)
  {
    LOG_SYSFATAL << "Failed in timerfd_create";
  }
  return timerfd;//返回timefd的文件描述符
}

struct timespec howMuchTimeFromNow(Timestamp when)//void resetTimerfd(...)需要的函数,用于计算timerfd的超时时间
{
  int64_t microseconds = when.microSecondsSinceEpoch()
                         - Timestamp::now().microSecondsSinceEpoch();
  if (microseconds < 100)
  {
    microseconds = 100;
  }
  struct timespec ts;
  ts.tv_sec = static_cast<time_t>(
      microseconds / Timestamp::kMicroSecondsPerSecond);
  ts.tv_nsec = static_cast<long>(
      (microseconds % Timestamp::kMicroSecondsPerSecond) * 1000);
  return ts;
}

void readTimerfd(int timerfd, Timestamp now)//
{
  uint64_t howmany;
  ssize_t n = ::read(timerfd, &howmany, sizeof howmany);
  LOG_TRACE << "TimerQueue::handleRead() " << howmany << " at " << now.toString();
  if (n != sizeof howmany)
  {
    LOG_ERROR << "TimerQueue::handleRead() reads " << n << " bytes instead of 8";
  }
}

void resetTimerfd(int timerfd, Timestamp expiration)//设置timerfd的超时时间,每次poll返回timerfd后都需要重新设置,以保持及时唤醒定时器!
{
  // wake up loop by timerfd_settime()
  struct itimerspec newValue;
  struct itimerspec oldValue;
  bzero(&newValue, sizeof newValue);
  bzero(&oldValue, sizeof oldValue);
  newValue.it_value = howMuchTimeFromNow(expiration);
  int ret = ::timerfd_settime(timerfd, 0, &newValue, &oldValue);
  if (ret)
  {
    LOG_SYSERR << "timerfd_settime()";
  }
}

}
}

using namespace muduo;
using namespace muduo::detail;

TimerQueue::TimerQueue(EventLoop* loop)
  : loop_(loop),//设置所属的EventLoop
    timerfd_(createTimerfd()),//创建一个,也是唯一一个timerfd,定时器文件描述符,用于poll的IO复用
    timerfdChannel_(loop, timerfd_),//创建一个Channel,Channel是联系class Poller唯一途径。
    timers_()//定时器class timer的队列
{
  timerfdChannel_.setReadCallback(
      boost::bind(&TimerQueue::handleRead, this));//设置Channel的可读事件的回调函数
  timerfdChannel_.enableReading();//向EventLoop的成员Poller进行注册timerfd
}

TimerQueue::~TimerQueue()
{
  ::close(timerfd_);//关闭tiemrfd,该资源由class TimerQueue创建,声明周期与其一致
  for (TimerList::iterator it = timers_.begin();
      it != timers_.end(); ++it)
  {
    delete it->second;//class Timer的实例由class TimerQueue创建,需要回收其资源
  }
}

TimerId TimerQueue::addTimer(const TimerCallback& cb,
                             Timestamp when,
                             double interval)//添加class timer的实例
{
  Timer* timer = new Timer(cb, when, interval);//需要负责其析构
  loop_->assertInLoopThread();
  bool earliestChanged = insert(timer);//插入set<entry>中,并返回一个bool值,表示是否需要重新设置timerfd的超时时间

  if (earliestChanged)
  {
    resetTimerfd(timerfd_, timer->expiration());
  }
  return TimerId(timer);
}

void TimerQueue::handleRead()//handleRead()为向Channel注册的可读事件的回调函数,主要功能是找到到期的Timer,并执行响应的Timer中的回调函数
{
  loop_->assertInLoopThread();
  Timestamp now(Timestamp::now());
  readTimerfd(timerfd_, now);

  std::vector<Entry> expired = getExpired(now);//getExpired()返回到期的Timer的vector

  // safe to callback outside critical section
  for (std::vector<Entry>::iterator it = expired.begin();
      it != expired.end(); ++it)
  {
    it->second->run();//执行Timer的回调函数
  }

  reset(expired, now);//重新将Timer加入到set<Entry>中,set会自动对定时器的时间卓先后顺序排序
}

std::vector<TimerQueue::Entry> TimerQueue::getExpired(Timestamp now)//返回到期的Timer的vector
{
  std::vector<Entry> expired;
  Entry sentry = std::make_pair(now, reinterpret_cast<Timer*>(UINTPTR_MAX));//设置到期的最大值
  TimerList::iterator it = timers_.lower_bound(sentry);//找到lower_bound
  assert(it == timers_.end() || now < it->first);
  std::copy(timers_.begin(), it, back_inserter(expired));//将到期Timer拷贝至vector<Entry> expired中
  timers_.erase(timers_.begin(), it);删除set<Entry>中到期Timer

  return expired;//返回到期的Timer
}

void TimerQueue::reset(const std::vector<Entry>& expired, Timestamp now)//将expired中的Timer根据循环间隔重新加入到set<Entry>的Timer队列
{
  Timestamp nextExpire;

  for (std::vector<Entry>::const_iterator it = expired.begin();
      it != expired.end(); ++it)
  {
    if (it->second->repeat())
    {
      it->second->restart(now);
      insert(it->second);
    }
    else
    {
      // FIXME move to a free list
      delete it->second;
    }
  }

  if (!timers_.empty())
  {
    nextExpire = timers_.begin()->second->expiration();
  }

  if (nextExpire.valid())
  {
    resetTimerfd(timerfd_, nextExpire);
  }
}

bool TimerQueue::insert(Timer* timer)//插入一个timer
{
  bool earliestChanged = false;
  Timestamp when = timer->expiration();//返回时间戳
  TimerList::iterator it = timers_.begin();
  if (it == timers_.end() || when < it->first)//set<Entry>timers_的第一Entry为最小的时间戳
  {
    earliestChanged = true;//需要重设timerfd的超时时间的bool标志
  }
  std::pair<TimerList::iterator, bool> result =
          timers_.insert(std::make_pair(when, timer));//将Timer插入到set<Entry>timers_中
  assert(result.second);
  return earliestChanged;//返回需要重设timerfd的超时时间的bool标志
}


洋洋洒洒标注了那么多,估计会看晕,但要点只有3个:

1、从构造函数上看,只有创建timerfd,创建Channel,注册回调函数和Channel,之后的事情就是其他class的问题了。

2、而另一个重要的功能就是添加定时器——TimerQueue::addTimer(const TimerCallback& cb,…),该函数添加一个定时器需要维护timerfd的超时时间(超时时间需要根据timers中最小的时间戳来决定),以及一个定时器列表,该列表根据时间戳从小到大排列,这也是为什么该列表使用set来实现的且set<pair<timestamp,timer*>>中pair<timestamp,timer*>不会出现重复。

3、timerfd超时后调用的回调函数handleRead(),该函数会从set<pair<timestamp,timer*>>取出到期的timer执行相应的回调函数timecallback(),执行完毕后计算timer下次到期的时间戳,再次加入到set<pair<timestamp,timer*>>中。

实际流程图如下:
构造函数注册
在这里插入图片描述
poll对timerfd超时后的调用顺序
在这里插入图片描述

addtimer函数的调用顺序就不写了,基本也就那么个顺序。
TimerQueue对外接口的函数只有一个,addtimer(),只要在EventLoop中加入TimerQueue,定义相关调用addtimer()成员函数就可实现定时任务。
EventLoop新增:

TimerId EventLoop::runAt(const Timestamp& time, const TimerCallback& cb)
{
  return timerQueue_->addTimer(cb, time, 0.0);
}

TimerId EventLoop::runAfter(double delay, const TimerCallback& cb)
{
  Timestamp time(addTime(Timestamp::now(), delay));
  return runAt(time, cb);
}

TimerId EventLoop::runEvery(double interval, const TimerCallback& cb)
{
  Timestamp time(addTime(Timestamp::now(), interval));
  return timerQueue_->addTimer(cb, time, interval);
}

private:
  boost::scoped_ptr<TimerQueue> timerQueue_;

至此定时器暂且还算是大功告成, 需要注意的是,为了不造成数据竞争等严重问题,addtimer()加入了loop_->assertInLoopThread(),因为如果有两个线程A,B,A线程调用addtimer(),B线程为EventLoop的所属线程,并且B线程正在调用了reset()或getExpired(Timestamp now),以上三个函数都会对set容器进行操作,线程安全不法保证! 但加入了断言,使得A线程无法随心所欲的使用addtimer等函数,解决方法是EventLoop加入一个函数队列,如果不在函数执行不在B线程中则加入B线程中EventLoop的函数队列,B线程在poll阻塞结束后执行完时间,再执行函数队列中的函数,当然得考虑B线程一直阻塞的情况,那么这个函数队列永远得不到执行,继而需要增加一个Channel,用于将poll从阻塞中及时唤醒。

那么新的EventLoop如下:

class EventLoop调整

class EventLoop : boost::noncopyable
{
 public:
  typedef boost::function<void()> Functor;

  EventLoop();
  ~EventLoop();
  void loop();
  void quit();
  Timestamp pollReturnTime() const { return pollReturnTime_; }
  void runInLoop(const Functor& cb);//可以暴露给其他线程的成员,线程安全性得以保证。
  void queueInLoop(const Functor& cb);//将函数加入函数队列
  TimerId runAt(const Timestamp& time, const TimerCallback& cb);
  TimerId runAfter(double delay, const TimerCallback& cb);
  TimerId runEvery(double interval, const TimerCallback& cb);
  void wakeup();//使线程从poll阻塞中返回
  void updateChannel(Channel* channel);

  void assertInLoopThread()
  {
    if (!isInLoopThread())
    {
      abortNotInLoopThread();
    }
  }

  bool isInLoopThread() const { return threadId_ == CurrentThread::tid(); }

 private:

  void abortNotInLoopThread();
  void handleRead();  // 事件回调
  void doPendingFunctors();//执行函数队列

  typedef std::vector<Channel*> ChannelList;

  bool looping_; /* atomic */
  bool quit_; /* atomic */
  bool callingPendingFunctors_; //正在执行函数队列的标识符,具有深意,能够及时的wakeup()
  const pid_t threadId_;
  Timestamp pollReturnTime_;
  boost::scoped_ptr<Poller> poller_;
  boost::scoped_ptr<TimerQueue> timerQueue_;
  int wakeupFd_;//文件描述符,属于EventLoop,对wakeupFd_写入后,poll会从阻塞中返回,及时的唤醒
  boost::scoped_ptr<Channel> wakeupChannel_;
  ChannelList activeChannels_;
  MutexLock mutex_;//锁
  std::vector<Functor> pendingFunctors_; // 函数队列
};
}

#endif

成员函数如下:

#include "EventLoop.h"

#include "Channel.h"
#include "Poller.h"
#include "TimerQueue.h"

#include "logging/Logging.h"

#include <boost/bind.hpp>

#include <assert.h>
#include <sys/eventfd.h>

using namespace muduo;

__thread EventLoop* t_loopInThisThread = 0;
const int kPollTimeMs = 10000;

static int createEventfd()//创建一个文件描述符,用于wakeupFd_
{
  int evtfd = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
  if (evtfd < 0)
  {
    LOG_SYSERR << "Failed in eventfd";
    abort();
  }
  return evtfd;
}

EventLoop::EventLoop()
  : looping_(false),
    quit_(false),
    callingPendingFunctors_(false),
    threadId_(CurrentThread::tid()),
    poller_(new Poller(this)),
    timerQueue_(new TimerQueue(this)),
    wakeupFd_(createEventfd()),//创建一个文件文件描述符
    wakeupChannel_(new Channel(this, wakeupFd_))//创建相应的 wakeupFd_的Channel
{
  LOG_TRACE << "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(
      boost::bind(&EventLoop::handleRead, this));//绑定回调函数
  // we are always reading the wakeupfd
  wakeupChannel_->enableReading();//向poller注册Channel
}

EventLoop::~EventLoop()
{
  assert(!looping_);
  ::close(wakeupFd_);
  t_loopInThisThread = NULL;
}

void EventLoop::loop()
{
  assert(!looping_);
  assertInLoopThread();
  looping_ = true;
  quit_ = false;

  while (!quit_)
  {
    activeChannels_.clear();
    pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
    for (ChannelList::iterator it = activeChannels_.begin();
        it != activeChannels_.end(); ++it)
    {
      (*it)->handleEvent();
    }
    doPendingFunctors();//新增,执行任务队列
  }

  LOG_TRACE << "EventLoop " << this << " stop looping";
  looping_ = false;
}

void EventLoop::quit()
{
  quit_ = true;
  if (!isInLoopThread())
  {
    wakeup();//新增,从poll阻塞唤醒当前线程,并结束loop循环
  }
}

void EventLoop::runInLoop(const Functor& cb)//暴露给外不线程的函数
{
  if (isInLoopThread())
  {
    cb();//在所属线程中直接执行
  }
  else
  {
    queueInLoop(cb);//加入所属的EventLoop的函数队列,并唤醒EventLoop的线程
  }
}

void EventLoop::queueInLoop(const Functor& cb)//加入所属的EventLoop的函数队列,并唤醒EventLoop的线程
{
  {
  MutexLockGuard lock(mutex_);//上锁
  pendingFunctors_.push_back(cb);
  }

  if (!isInLoopThread() || callingPendingFunctors_)//唤醒线程的条件中有callingPendingFunctors_,因为如果执行函数队列PendingFunctors_时有可能调用queueinloop(),!isInLoopThread()部分返回false,,无法执行wakeup(),无法被及时的唤醒
  {
    wakeup();
  }
}

TimerId EventLoop::runAt(const Timestamp& time, const TimerCallback& cb)
{
  return timerQueue_->addTimer(cb, time, 0.0);
}

TimerId EventLoop::runAfter(double delay, const TimerCallback& cb)
{
  Timestamp time(addTime(Timestamp::now(), delay));
  return runAt(time, cb);
}

TimerId EventLoop::runEvery(double interval, const TimerCallback& cb)
{
  Timestamp time(addTime(Timestamp::now(), interval));
  return timerQueue_->addTimer(cb, time, interval);
}

void EventLoop::updateChannel(Channel* channel)
{
  assert(channel->ownerLoop() == this);
  assertInLoopThread();
  poller_->updateChannel(channel);
}

void EventLoop::abortNotInLoopThread()
{
  LOG_FATAL << "EventLoop::abortNotInLoopThread - EventLoop " << this
            << " was created in threadId_ = " << threadId_
            << ", current thread id = " <<  CurrentThread::tid();
}

void EventLoop::wakeup()//唤醒,对wakeupFd_文件描述符写入,poll从监听阻塞中返回事件的发生
{
  uint64_t one = 1;
  ssize_t n = ::write(wakeupFd_, &one, sizeof one);
  if (n != sizeof one)
  {
    LOG_ERROR << "EventLoop::wakeup() writes " << n << " bytes instead of 8";
  }
}

void EventLoop::handleRead()//回调函数
{
  uint64_t one = 1;
  ssize_t n = ::read(wakeupFd_, &one, sizeof one);
  if (n != sizeof one)
  {
    LOG_ERROR << "EventLoop::handleRead() reads " << n << " bytes instead of 8";
  }
}

void EventLoop::doPendingFunctors()//执行函数队列,
{
  std::vector<Functor> functors;
  callingPendingFunctors_ = true;

  {
  MutexLockGuard lock(mutex_);
  functors.swap(pendingFunctors_);//交换函数队列,减少锁的持有时间,并且避免了函数队列中函数有可能执行queueinloop(),造成无限制的死循环的情况。
  }

  for (size_t i = 0; i < functors.size(); ++i)
  {
    functors[i]();
  }
  callingPendingFunctors_ = false;
}

有了runInLoop()函数就可以对EventLoop的公有函数进行调整,实现公有函数在不同线程调用的安全性。

代码如下,因为很简单就不注释了:

TimerId TimerQueue::addTimer(const TimerCallback& cb,
                             Timestamp when,
                             double interval)
{
  Timer* timer = new Timer(cb, when, interval);
  loop_->runInLoop(
      boost::bind(&TimerQueue::addTimerInLoop, this, timer));
  return TimerId(timer);
}

void TimerQueue::addTimerInLoop(Timer* timer)
{
  loop_->assertInLoopThread();
  bool earliestChanged = insert(timer);

  if (earliestChanged)
  {
    resetTimerfd(timerfd_, timer->expiration());
  }
}

在线程中创建并执行EventLoop

class EventLoopThread
上文EventLoop已经实现了大部分的功能,但无法在线程中使用,具体以代码展示来说明:

#include "EventLoop.h"
#include <stdio.h>

muduo::EventLoop* g_loop;
int g_flag = 0;

void run4()
{
  printf("run4(): pid = %d, flag = %d\n", getpid(), g_flag);
  g_loop->quit();
}

void run3()
{
  printf("run3(): pid = %d, flag = %d\n", getpid(), g_flag);
  g_loop->runAfter(3, run4);
  g_flag = 3;
}

void run2()
{
  printf("run2(): pid = %d, flag = %d\n", getpid(), g_flag);
  g_loop->queueInLoop(run3);
}

void run1()
{
  g_flag = 1;
  printf("run1(): pid = %d, flag = %d\n", getpid(), g_flag);
  g_loop->runInLoop(run2);
  g_flag = 2;
}

int main()
{
  printf("main(): pid = %d, flag = %d\n", getpid(), g_flag);

  muduo::EventLoop loop;
  g_loop = &loop;

  loop.runAfter(2, run1);
  loop.loop();
  printf("main(): pid = %d, flag = %d\n", getpid(), g_flag);
}

在该程序中loop只能在该进程中执行,无法直接放入一个新线程中执行,或者说需要每次写一个函数来执行EventLoop的创建和运行,再将这个函数放入新线程中执行,这样不如直接提供一个线程运行的class,用于在新线程中创建EventLoop的实例,并执行loop(),返回EventLoop的指针,代码如下:


#ifndef MUDUO_NET_EVENTLOOPTHREAD_H
#define MUDUO_NET_EVENTLOOPTHREAD_H

#include "thread/Condition.h"
#include "thread/Mutex.h"
#include "thread/Thread.h"

#include <boost/noncopyable.hpp>

namespace muduo
{

class EventLoop;

class EventLoopThread : boost::noncopyable
{
 public:
  EventLoopThread();
  ~EventLoopThread();
  EventLoop* startLoop();

 private:
  void threadFunc();

  EventLoop* loop_;
  bool exiting_;
  Thread thread_;
  MutexLock mutex_;
  Condition cond_;
};

}

#endif 

成员函数如下:

EventLoopThread::EventLoopThread()
  : loop_(NULL),
    exiting_(false),
    thread_(boost::bind(&EventLoopThread::threadFunc, this)),//新线程执行函数的绑定
    mutex_(),
    cond_(mutex_)
{
}

EventLoopThread::~EventLoopThread()
{
  exiting_ = true;
  loop_->quit();
  thread_.join();
}

EventLoop* EventLoopThread::startLoop()//启动线程
{
  assert(!thread_.started());
  thread_.start();

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

  return loop_;
}

void EventLoopThread::threadFunc()//创建EventLoop,并运行loop
{
  EventLoop loop;

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

  loop.loop();
  //assert(exiting_);
}

至此,muduo网络库的基本框架已经介绍完毕,第二章网络库的实现同样是基于第一章的结构之上。。。

未完待续

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

muduo网络库浅谈(一) 的相关文章

  • 在 HKCR 中创建新密钥有效,但不起作用

    我有以下代码 它返回 成功 但使用两种不同的工具使用搜索字符串 3BDAAC43 E734 11D5 93AF 00105A990292 搜索注册表不会产生任何结果 RegistryKey RK Registry ClassesRoot C
  • 现代 C++ 编译器是否能够在某些情况下避免调用 const 函数两次?

    例如 如果我有以下代码 class SomeDataProcessor public bool calc const SomeData d1 const SomeData d2 const private Some non mutable
  • MVC3中设置下拉列表中的所选项目

    我必须为视图中的下拉列表设置所选项目 但它不起作用 View div class editor label Html LabelFor model gt model Gender div div class editor field Htm
  • 循环遍历 C 结构中的元素以提取单个元素的值和数据类型

    我有一个要求 我有一个 C 语言的大结构 由大约 30 多个不同数据类型的不同元素组成 typedef struct type1 element1 type2 element2 type3 element3 type2 element4 1
  • 传递 constexpr 对象

    我决定给予新的C 14的定义constexpr旋转并充分利用它 我决定编写一个小的编译时字符串解析器 然而 我正在努力保持我的对象constexpr将其传递给函数时 考虑以下代码 include
  • 如何将 SOLID 原则应用到现有项目中

    我对这个问题的主观性表示歉意 但我有点卡住了 我希望之前处理过这个问题的人能够提供一些指导和建议 我有 现在已经成为 一个用 C 2 0 编写的非常大的 RESTful API 项目 并且我的一些类已经变得巨大 我的主要 API 类就是一个
  • extern 声明和函数定义都在同一文件中

    我只是浏览了一下gcc源文件 在gcc c 我发现了类似的东西 extern int main int char int main int argc char argv 现在我的疑问是extern是告诉编译器特定的函数不在这个文件中 但可以
  • 如何在 C# Designer.cs 代码中使用常量字符串?

    如何在 designer cs 文件中引用常量字符串 一个直接的答案是在我的 cs 文件中创建一个私有字符串变量 然后编辑 Designer cs 文件以使用此变量 而不是对字符串进行硬编码 但设计者不喜欢这样抛出错误 我明白为什么这行不通
  • 即使没有异步,CallContext.LogicalGetData 也会恢复。为什么?

    我注意到CallContext LogicalSetData LogicalGetData不按照我期望的方式工作 内部设置的值async方法得到恢复即使没有异步或任何类型的线程切换 无论如何 这是一个简单的例子 using System u
  • 如何使用 Regex.Replace 从字符串中删除数字?

    我需要使用Regex Replace从字符串中删除所有数字和符号 输入示例 123 abcd33输出示例 abcd 请尝试以下操作 var output Regex Replace input d string Empty The d标识符
  • 什么是空终止字符串?

    它与什么不同标准 字符串 http www cplusplus com reference string string 字符串 实际上只是一个数组chars 空终止字符串是指其中包含空字符的字符串 0 标记字符串的结尾 不一定是数组的结尾
  • 如果输入被重定向则执行操作

    我想知道如果我的输入被重定向 我应该如何在 C 程序中执行操作 例如 假设我有已编译的程序 prog 并且我将输入 input txt 重定向到它 我这样做 prog lt input txt 我如何在代码中检测到这一点 一般来说 您无法判
  • 在 C 中使用枚举而不是 #defines 作为编译时常量是否合理?

    在 C 工作了一段时间后 我将回到 C 开发领域 我已经意识到 在不必要的时候应该避免使用宏 以便让编译器在编译时为您做更多的工作 因此 对于常量值 在 C 中我将使用静态 const 变量或 C 11 枚举类来实现良好的作用域 在 C 中
  • 比较:接口方法、虚方法、抽象方法

    它们各自的优点和缺点是什么 接口方法 虚拟方法 抽象方法 什么时候应该选择什么 做出这一决定时应牢记哪些要点 虚拟和抽象几乎是一样的 虚方法在基类中有一个实现 可以选择重写 而抽象方法则没有 并且must在子类中被覆盖 否则它们是相同的 在
  • C++:为什么 numeric_limits 对它不知道的类型起作用?

    我创建了自己的类型 没有任何比较器 也没有专门化std numeric limits 尽管如此 由于某种原因 std numeric limits
  • WPF DataGrid / ListView 绑定到数组 mvvm

    我们假设你有 N 个整数的数组 表示行数的整数值 在模型中 该整数绑定到视图中的 ComboBox Q1 如何将数组 或数组的各个项目 绑定到 DataGrid 或 ListView 控件 以便 当您更改 ComboBox 值时 只有那么多
  • 代码中的.net Access Forms身份验证“超时”值

    我正在向我的应用程序添加注销过期警报 并希望从我的代码访问我的 web config 表单身份验证 超时 值 我有什么办法可以做到这一点吗 我认为您可以从 FormsAuthentication 静态类方法中读取它 这比直接读取 web c
  • 如何在 sql azure 上运行 aspnet_regsql? [复制]

    这个问题在这里已经有答案了 可能的重复 将 ASP NET 成员资格数据库迁移到 SQL Azure https stackoverflow com questions 10140774 migrating asp net membersh
  • 为什么空循环使用如此多的处理器时间?

    如果我的代码中有一个空的 while 循环 例如 while true 它将把处理器的使用率提高到大约 25 但是 如果我执行以下操作 while true Sleep 1 它只会使用大约1 那么这是为什么呢 更新 感谢所有精彩的回复 但我
  • 是否允许全局静态标识符以单个 _ 开头?

    换句话说 可能static 文件范围 全局变量恰好以一个下划线开头 而不会产生与 C 实现发生名称冲突的可能性 https www gnu org software libc manual html node Reserved Names

随机推荐

  • shell 脚本中 $$、$#、$? 分别代表什么意思?

    0 这个程式的执行名字 n 这个程式的第 n 个参数值 n 1 9 这个程式的所有参数 此选项参数可超过 9 个 这个程式的参数个数 这个程式的 PID 脚本运行的当前进程 ID 号 执行上一个背景指令的 PID 后台运行的最后一个进程的进
  • Ubuntu16.04 ARM/Qt 交叉编译环境搭建

    嵌入式开发 Ubuntu16 04 ARM Qt 交叉编译环境搭建 背景 环境说明 安装交叉编译工具 下载Qt源码包 编译Qt源码 安装QtCreator 配置QtCreator 应用QtCreator交叉编译 Ubuntu16 04 AR
  • springboot配置双mysql数据源

    这两天一直在配置双数据源 找了网上很多资料 有的资料写的太乱而且注释不清楚 类不全 像我这样的刚开始配置的新手很难看明白 今天终于配置成功了 我把我总结的整理一下 做个日志以防以后遇到问题 一 创建一个springboot项目其中需要的po
  • h5页面判断 js判断 是否安装APP,如果安装就拉起APP 打开app ,否则就下载

    h5页面判断是否安装APP 如果安装就拉起APP 否则就下载 if navigator userAgent match iPhone iPod iPad i var loadDateTime new Date window location
  • PHP[多维数组转字符串]和{多维数组转一维数组}

    http blog csdn net aoyoo111 article details 8554585 php view plain copy print method 多维数组转字符串 param type array return ty
  • 基于Echarts4.0实现旭日图

    昨天Echarts4 0正式发布 随着4 0而来的是一系列的更新 挑几个主要的简单说明 1 展示方面通过增量渲染技术 4 0 ECharts 能够展现千万级的数据量 2 针对移动端优化 移动端小屏上适于用手指在坐标系中进行缩放 平移 可选的
  • 探索AIDL(1) -- 初识AIDL

    探索AIDL 1 初识AIDL 前言 1 在讨论这个问题之前必须先理解IPC的概念 IPC全称是Inner Process Communication 即跨进程通信 是指两个进程进行数据交换的过程 2 如果要传递对象必须先进行序列化 序列化
  • RV1126 SDK编译错误及解决记录

    RV1126 SDK编译错误及解决记录 1 错误 you need to install unbuffer from package expect or expect dev log saved on home h00003 RV1126
  • Win11怎么设置开机启动项?

    我们在使用电脑的时候经常会打开非常多的软件 而每次开机都需要手动去点击 就会变得非常的麻烦 那么在Win11操作系统中我们应该怎么设置呢 其实方法非常简单 下面小编就带着大家一起看看吧 操作方法 方法一 1 首先 按键盘上的 Win 键 或
  • 关于线索二叉树的详解

    线索二叉树的详解 目录 线索二叉树的详解 前言 一 线索二叉树是什么 三种二叉树线索化实例图 二 实现线索二叉树 1 二叉树的线索化 2 线索二叉树的遍历 中序线索二叉树寻找遍历的首节点 中序线索二叉树寻找节点的直接后继 遍历中序线索二叉树
  • 【国仁网络资讯】个人如何快速学会制作短视频的方法技巧

    对于微信视频号运营中 个人创作者们了解到视频号的一般设置选项 以及选择完毕视频号的创作领域方向之后 那就需要创作出短视频出来 就好比我们写文章需要有作品才可以 如果我们有创作能力的 那可以根据我们标准的风格 以及创作标准元素创作和分享就可以
  • python 读取图片 图片预处理 二值化

    python 读取图片 图片预处理 二值化 需求 毕设项目 需要从文件夹中批量读取文件 并将图片二值化处理 def preprocessing image directory in name directory out name 准备储存之
  • Firefox 中文语言包安装方法

    一 自动安装法 在mozilla的FTP上找到的 http ftp mozilla org 选择版本和对应操作系统http releases mozilla org pub mozilla org firefox releases 语言包在
  • dd命令使用详解

    1 命令简介 dd 的主要选项 指定数字的地方若以下列字符结尾乘以相应的数字 b 512 c 1 k 1024 w 2 xm number m if file 输入文件名 缺省为标准输入 of file 输出文件名 缺省为标准输出 ibs
  • HTML <rt> 标签

    实例 一个 ruby 注释
  • Verilog学习笔记——有符号数的乘法和加法

    有符号数的计算在 Verilog 中是一个很重要的问题 也很容易会被忽视 在使用 Verilog 语言编写 FIR 滤波器时 需要涉及到有符号数的加法和乘法 在之前的程序中我把所有的输入输出和中间信号都定义成有符号数 这样在计算时没有出现问
  • JS实现抽奖效果

    这种效果很常见 所以对我们前段开发者来说是基本功 必须掌握 页面布局 实现思路如图 一条流水线的思路 代码也很简单啦 部分 div div
  • 清华大数据,365天我们持续在发声——数据院四周年系列报道之传播篇

    2014年 宏大的时代又催生了一个弱小的组织 清华大学数据科学研究院 这个 初来乍到 的非实体机构 在清华大学官网首页院系设置的树状图上我找不到她的 身影 平心而论 技术进步这么快 校内类似的机构有很多 如果都放上去 那将是一棵枝叶茂密的参
  • AttributeError: ‘Axes‘ object has no property ‘axisbg‘

    在使用python画图的时候报了这些错 说明axibg这个属性已经被取消了 定位报错的axibg 将其改为fc就可以了 正确的如下
  • muduo网络库浅谈(一)

    muduo网络库浅谈 一 序言 第一章 muduo的关键结构 class EventLoop class Channel class Poller 番外 定时任务 class Timestamp class Timer class Time