muduo网络库源码解析(1):多线程异步日志库(上)
muduo网络库源码解析(2):多线程异步日志库(中)
muduo网络库源码解析(3):多线程异步日志库(下)
muduo网络库源码解析(4):TimerQueue定时机制
muduo网络库源码解析(5):EventLoop,Channel与事件分发机制
muduo网络库源码解析(6):TcpServer与TcpConnection(上)
muduo网络库源码解析(7):TcpServer与TcpConnection(下)
muduo网络库源码解析(8):EventLoopThreadPool与EventLoopThread
muduo网络库源码解析(9):Connector与TcpClient
引言
前两篇文章分析了muduo日志库的同步部分与异步部分,这篇文章算是对日志部分的一个总结,同时加入我对于muduo日志库的几点修改建议及实现,笔者水平有限,欢迎大家的讨论和提出宝贵的意见!
这篇文章主要是对两个地方提出修改的建议,其中一个地方已做出修改,下面给出了做法和修改的理由.和另一个地方在经过了一些尝试和权衡后并没有修改,同样也会给出理由,先抛出两个修改的地方.
- logger在每条日志都会产生一个对象,计划改成单例.
- asynclogging中多线程写入使用单个的全局锁作为同步工具,在多线程中并发潜力较低,可改为线程安全的hashmap,给桶上锁,这样的话可以真正做到并发.
单例
这个显然是可行的,但不一定会有多么大的性能提升,你可能会说这还不大吗,二百万次条日志就有二百万次的构造,但是实际上比起整个结构上的开销来说确实不算大,但是一定会有提升,我们先来看看一次日志的生成是如何完成的,
#define LOG_TRACE if (muduo::Logger::logLevel() <= muduo::Logger::TRACE) \
muduo::Logger(__FILE__, __LINE__, muduo::Logger::TRACE, __func__).stream()
#define LOG_DEBUG if (muduo::Logger::logLevel() <= muduo::Logger::DEBUG) \
muduo::Logger(__FILE__, __LINE__, muduo::Logger::DEBUG, __func__).stream()
#define LOG_INFO if (muduo::Logger::logLevel() <= muduo::Logger::INFO) \
muduo::Logger(__FILE__, __LINE__).stream()
#define LOG_WARN muduo::Logger(__FILE__, __LINE__, muduo::Logger::WARN).stream()
#define LOG_ERROR muduo::Logger(__FILE__, __LINE__, muduo::Logger::ERROR).stream()
#define LOG_FATAL muduo::Logger(__FILE__, __LINE__, muduo::Logger::FATAL).stream()
#define LOG_SYSERR muduo::Logger(__FILE__, __LINE__, false).stream()
#define LOG_SYSFATAL muduo::Logger(__FILE__, __LINE__, true).stream()
就是生成一个Logger,在Logger的构造中写入一些消息,然后返回一个流,因为重载了>>,我们这可以在其中写入内容,然后在析构实在写入文件名和行数,清晰简介,但是却有不少多余的构造,我们可以看到每一个对象之间其实没有必然的联系,且处理逻辑都是相同的,意味着我们可以写一个单例的Logger工厂,这样就可以保证全局只有一个了,但是有一个问题就是既然析构的过程也会写入内容,没有析构了怎么办,我们可以这样解决,在工厂中得到兑现后进行写入,这样就可以保证全部正确了,我们来看看具体的实现.
template<typename logging::Loglevel LEVEL>
class loggingFactory : public Nocopy{
private:
std::shared_ptr<logging> LogData;
std::once_flag resourse_flag;
void initResourse(logging::Filewrapper file, int line, typename ws::detail::logging::Loglevel level);
public:
logging& getStream(logging::Filewrapper file, int line, int olderrno, typename ws::detail::logging::Loglevel level = logging::INFO);
~loggingFactory();
};
logging& log_DEBUG(logging::Filewrapper file, int line, int olderrno);
logging& log_INFO(logging::Filewrapper file, int line, int olderrno);
logging& log_WARN(logging::Filewrapper file, int line, int olderrno);
logging& log_ERROR(logging::Filewrapper file, int line, int olderrno);
logging& log_FATAL(logging::Filewrapper file, int line, int olderrno);
在Logger工厂的实现上我选择了模板非类型参数,这样可以更好的代码复用,不必每一个日志等级都设置一个工厂,然后宏函数我改为了一个单例的工厂函数,以保证全局拿到一个实例,我们来看看具体的实现
template<typename logging::Loglevel LEVEL>
void
loggingFactory<LEVEL>::initResourse(logging::Filewrapper file, int line, typename ws::detail::logging::Loglevel level){
LogData.reset(new logging(file, line, level));
}
template<typename logging::Loglevel LEVEL>
logging&
loggingFactory<LEVEL>::getStream(logging::Filewrapper file, int line, int old_errno, typename ws::detail::logging::Loglevel level){
std::call_once(resourse_flag, &loggingFactory::initResourse, this, file, line, level);
logstream& Stream = LogData->stream();
LogData->wrapper_.time_.swap(Timestamp::now());
LogData->wrapper_.formatTime();
Stream << helper(LogLevelName[static_cast<size_t>(level)], 6);
if (old_errno != 0){
Stream << strerror_tl(old_errno) << " (errno=" << old_errno << ") ";
}
LogData->wrapper_.finish();
const logstream::Buffer& buf(LogData->stream().buffer());
g_output_(buf.data(), buf.Length());
LogData->stream().resetBuffer();
if(LogData->wrapper_.level_ == logging::FATAL){
g_flush_();
abort();
}
return *LogData;
}
用std::call_once与std::once_flag保证资源仅初始化一次,高效又不用使用臭名昭著的DCL,我们再来看看log_xxx的实现,
logging& log_DEBUG(logging::Filewrapper file, int line, int olderrno){
static loggingFactory<logging::DEBUG> loog;
return loog.getStream(file, line, olderrno, logging::DEBUG);
}
如上所见,我们实际只需要拿到单例的对象后使用getstream即可,static本身在C++以后的资源初始化也是保证线程安全的.
到了这里基本问题已经解决,但还有一点,就是使用后我们去执行会发现少了一条日志,也就是最后一条日志,我们注意到getstream中有一句这样的语句==g_output_(buf.data(), buf.Length());==意味着倒数第一条语句的内容之前的语句已经写入,而内容还存在在缓冲区中,我们的做法就是在工厂的析构中再进行一次刷新.
template<typename logging::Loglevel LEVEL>
loggingFactory<LEVEL>::~loggingFactory(){
const logstream::Buffer& buf(LogData->stream().buffer());
g_output_(buf.data(), buf.Length());
}
至此,第一条修改已经完成,我们不妨来看看性能对比:
测试代码
std::unique_ptr<ws::detail::logfile> g_logFile;
void outputFunc(const char* msg, int len)
{
g_logFile->append(msg, len);
}
void flushFunc()
{
g_logFile->flush();
}
int main(int argc, char* argv[])
{
char name[256] = { 0 };
strncpy(name, argv[0], sizeof name - 1);
g_logFile.reset(new ws::detail::logfile(::basename(name), 200*1000));
ws::detail::logging::setOutput(outputFunc);
ws::detail::logging::setFlush(flushFunc);
std::string line = "1234567890 abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ ";
for (int i = 0; i < 10000; ++i)
{
ws::detail::log_INFO(__FILE__, __LINE__, errno).stream() << line << i << ":" << "\n";
usleep(1000);
}
}
以上是修改后的,
我们可以看到效率上没有太大的差异.
这些是没修改的,不带sleep的版本,测试的图片我忘存了,原版本代码也删了,但是结果我留了,
就是写二百万条数据的测试
我们来看看吧
测试 | 一条日志写入时间 |
---|---|
修改前 | 1.18μs |
修改后 | 1.16μs |
我们可以看到是有提升,但并不明显,这也是可以想象的.这样看来二八法则仍是我们代码过程中一个重要的参考法则.
这是我做了好几组测试后的结果,写入效率大概是100MB/s.
来看看测试代码
for (int i = 0; i < 2000000; ++i)
{
ws::detail::log_INFO(__FILE__, __LINE__, errno).stream() << line << i << ":" << "\n";
}
其实就是改为二百万条,sleep去掉
hash_map
这里我并没有实际的写出一个版本去测试,原因其一在于是我不觉得的这在我的需求中完成后效率会有多少提升,甚至会有下降.其二在于我去优化这个的目的在与使我写的服务器能够效率更高,但是在目前情况下来看我的机器是四核的,其实并没有什么必要,就算升级到八核才会有一定的提升,而且也不一定有多么大,为什么呢,我们来看一看.
我们先来贴出我觉得可以优化的地方,即AsyncLogging::threadFunc与,AsyncLogging::append,我们来看具体片段
{
muduo::MutexLockGuard lock(mutex_);
if (buffers_.empty()) // unusual usage!
{
cond_.waitForSeconds(flushInterval_);
}
buffers_.push_back(std::move(currentBuffer_));
currentBuffer_ = std::move(newBuffer1);
buffersToWrite.swap(buffers_);
if (!nextBuffer_)
{
nextBuffer_ = std::move(newBuffer2);
}
}
{
buffers_.push_back(std::move(currentBuffer_));
if(nextBuffer_){
currentBuffer_ = std::move(nextBuffer_);
}else{
currentBuffer_.reset(new Buffer);
}
currentBuffer_->append(line, len);
cv.notify_one();
}
我们可以看到这其实就是一个典型的生产者消费者模式,所有的写日志线程是生产者,异步日志线程是消费者.其实用单个锁来干这件事情效率低是可以想象的.我们来简要的分析一下.
- 其一是锁的竞争,这样利用系统的调度很可能导致某个线程的饥饿.
- 其二单个锁必然导致锁的粒度大,但是这个问题代码中已经用交换指针和移动语义解决.
- 最重要也是最后一点,其三:并发潜力不高
第三点其实与第一点有一定的重叠,但还是值得一说,这是什么意思呢,使用单个锁必然导致所有线程的同步依赖于这个锁,其实我们细看异步线程的结构,可以并发的部分其实只有append部分,异步线程的处理即写入只能在一个异步线程中完成,所以要提高并发度我们只能从append入手,如何做?目前我能想到两个方法,即线程安全的hashmap与高并发潜力版本的blockingqueue.如何做?我们先来看看hash_map,就是在正常的append的push_back部分改成hashmap的insert,threadFunc中仍然需要wait_for,这就意味着我们需要自己实现一个hash_map中自带一个条件变量才能实现,使得这确实可以提升效率,但是在写入部分呢?正常的的情况下我们需要遍历每一个桶,拿出其中数据,再push_back入buffersToWrite中,为了方便swap,我们也可以把buffersToWrite也写成类似于邻接表那样.但是我们在取出数据的时候需要给每一个桶加锁取出数据,这也是一个消耗.但是恕我直言,二八法则依旧是值得借鉴的,当发现这是瓶颈的时候再改也不迟,这显然可以提升效率,但可能和第一条修改是一样的.对于第二条修改,以上是我的想法.
这是一个线程安全的hash_map,可能在发现这一条是瓶颈时我会去改吧.
#ifndef THREADSAFE_UNORDERED_MAP_H_
#define THREADSAFE_UNORDERED_MAP_H_
#include <algorithm> //find_if
#include <vector> //vector
#include <map> //map
#include <list> //list
#include <memory> //unique_ptr
#include <mutex> //lock_guard
#include <boost/thread/shared_mutex.hpp> // shared_mutex
namespace ws{
namespace detail{
template<typename Key, typename Value, typename Hash = std::hash<Key>>
class Threadsafe_unordered_map{
private:
class buket_type{
friend class Threadsafe_unordered_map;
private:
using buket_value = std::pair<Key, Value>;
using buket_data = std::list<buket_value>;
using buket_iterator = typename buket_data::iterator;
buket_data data;
mutable boost::shared_mutex mutex;
buket_iterator
find_entry(const Key& key) {
return std::find_if(
data.begin(), data.end(),
[&](const buket_value& para){
return para.first == key;
}
);
}
public:
Value value_of(const Key& key, const Value& default_value) const {
boost::shared_lock<boost::shared_mutex> lk(mutex);
const buket_iterator value_iterator = find_entry(key);
return (value_iterator == data.end()) ?
default_value : value_iterator.second;
}
void add_or_update(const Key& key, const Value& value){
std::lock_guard<boost::shared_mutex> guard(mutex);
const buket_iterator value_iterator = find_entry(key);
if(value_iterator == data.end()){
data.push_back(buket_value(key, value));
}else{
value_iterator->second = value;
}
}
void remove(const Key& key){
std::unique_lock<boost::shared_mutex> guard(mutex);
const buket_iterator value_iterator = find_entry(key);
if(value_iterator != data.end()){
data.erase(value_iterator);
}
}
};
std::vector<std::unique_ptr<buket_type>> bukets;
Hash hasher;
buket_type& get_buket(const Key& key) const {
const std::size_t index = hasher(key) % bukets.size();
return *bukets[index];
}
public:
using key_type = Key;
using value_type = Value;
using hash_type = Hash;
explicit Threadsafe_unordered_map(unsigned num_bukets = 23, const Hash& hasher_ = Hash())
: hasher(hasher_), bukets(num_bukets){
for(size_t i = 0; i < num_bukets; i++){
bukets[i].reset(new buket_type);
}
}
Threadsafe_unordered_map(const Threadsafe_unordered_map&) = delete;
Threadsafe_unordered_map& operator=(
const Threadsafe_unordered_map&
) = delete;
value_type value_of(const key_type& key, const value_type& value = value_type()) const {
return get_buket(key).value_of(key, value);
}
void add_or_update(const key_type& key, const value_type& value){
return get_buket(key).add_or_update(key, value);
}
void remove(const key_type& key){
return get_buket(key).remove(key);
}
std::map<Key, Value>
get_standard_map() const;
};
template<typename Key, typename Value, typename Hash>
std::map<Key, Value>
Threadsafe_unordered_map<Key, Value, Hash>::get_standard_map() const {
std::map<key_type, value_type> instance;
std::vector<std::unique_lock<boost::shared_mutex>> lk;
for(size_t i = 0; i < bukets.size(); i++){
lk.emplace_back(std::unique_lock<boost::shared_mutex>(bukets[i]->mutex));
}
for(size_t i = 0; i < bukets.size(); i++){
for(typename buket_type::buket_iterator bt = bukets[i]->data.begin();
bt != bukets[i]->data.end(); ++bt){
instance.insert(*bt);
}
}
return std::move(instance);
}
}
}
#endif //THREADSAFE_UNORDERED_MAP_H_
在这个场景下读写锁改成一般的互斥锁即可,只需要加上一个条件变量就可以直接使用了.
总结
优化并不是嘴上说说,是要有性能比较的,要不就是耍流氓…就比如说第一个测试.再一点就是,二八法则请谨记于心,把更多的经历放在更重要的结构上,而不是对整体性能来说鸡毛蒜皮的边边角角上,那些有时间再改也不迟.