save variable argument list for fprintf calls

大兔子大兔子 提交于 2019-12-02 03:36:38

I don't see how this would be done easily using the legacy C vprintf with va_lists. As you want to pass things around between threads, sooner or later you will need to use the heap in some way.

Below is a solution that uses Boost.Format for the formatting and Boost.Variant for parameter passing. The example is complete and working if you concatenate the following code blocks in order. If you compile with GCC, you need to pass the -pthread linker flag. And of course, you'll also need the two Boost libraries which are header-only, however. Here are the headers we will use.

#include <condition_variable>
#include <iostream>
#include <list>
#include <locale>
#include <mutex>
#include <random>
#include <string>
#include <thread>
#include <utility>
#include <vector>

#include <boost/format.hpp>
#include <boost/variant.hpp>

At first, we need some mechanism to asynchronously execute some tasks, in this case, print our logging messages. Since the concept is general, I use an “abstract” base class Spooler for this. Its code is based on Herb Sutter's talk “Lock-Free Programming (or, Juggling Razor Blades)” on CppCon 2014 (part 1, part 2). I'm not going into detail about this code because it is mostly scaffolding not directly related to your question and I assume you already have this piece of functionality in place. My Spooler uses a std::list protected by a std::mutex as a task queue. It might be worthwhile to consider using a lock-free data structure instead.

class Spooler
{
private:

  bool done_ {};
  std::list<std::function<void(void)>> queue_ {};
  std::mutex mutex_ {};
  std::condition_variable condvar_ {};
  std::thread worker_ {};

public:

  Spooler() : worker_ {[this](){ work(); }}
  {
  }

  ~Spooler()
  {
    auto poison = [this](){ done_ = true; };
    this->submit(std::move(poison));
    if (this->worker_.joinable())
      this->worker_.join();
  }

protected:

  void
  submit(std::function<void(void)> task)
  {
    // This is basically a push_back but avoids potentially blocking
    // calls while in the critical section.
    decltype(this->queue_) tmp {std::move(task)};
    {
      std::unique_lock<std::mutex> lck {this->mutex_};
      this->queue_.splice(this->queue_.cend(), tmp);
    }
    this->condvar_.notify_all();
  }

private:

  void
  work()
  {
    do
      {
        std::unique_lock<std::mutex> lck {this->mutex_};
        while (this->queue_.empty())
          this->condvar_.wait(lck);
        const auto task = std::move(this->queue_.front());
        this->queue_.pop_front();
        lck.unlock();
        task();
      }
    while (!this->done_);
  }
};

From the Spooler, we now derive a Logger that (privately) inherits its asynchronous capabilities from the Spooler and adds the logging specific functionality. It has only one function member called log that takes as parameters a format string and zero or more arguments to format into it as a std::vector of boost::variants.

Unfortunately, this limits us to a fixed number of types we can support but that shouldn't be a large problem since the C printf doesn't support arbitrary types either. For the sake of this example, I'm only using int and double but you can extend the list with std::strings, void * pointers or what have you.

The log function constructs a lambda expression that creates a boost::format object, feeds it all the arguments and then writes it to std::log or wherever you want the formatted message to go.

The constructor of boost::format has an overload that accepts the format string and a locale. You might be interested in this one since you have mentioned setting a custom locale in the comments. The usual constructor only takes a single argument, the format string.

Note how all formatting and outputting is done on the spooler's thread.

class Logger : Spooler
{
 public:

  void
  log(const std::string& fmt,
      const std::vector<boost::variant<int, double>>& args)
  {
    auto task = [fmt, args](){
      boost::format msg {fmt, std::locale {"C"}};  // your locale here
      for (const auto& arg : args)
        msg % arg;  // feed the next argument
      std::clog << msg << std::endl;  // print the formatted message
    };
    this->submit(std::move(task));
  }
};

This is all it takes. We can now use the Logger like in this example. It is important that all worker threads are join() ed before the Logger is destructed or it won't process all messages.

int
main()
{
  Logger logger {};
  std::vector<std::thread> threads {};
  std::random_device rnddev {};
  for (int i = 0; i < 4; ++i)
    {
      const auto seed = rnddev();
      auto task = [&logger, i, seed](){
        std::default_random_engine rndeng {seed};
        std::uniform_real_distribution<double> rnddist {0.0, 0.5};
        for (double p = 0.0; p < 1.0; p += rnddist(rndeng))
          logger.log("thread #%d is %6.2f %% done", {i, 100.0 * p});
        logger.log("thread #%d has completed its work", {i});
      };
      threads.emplace_back(std::move(task));
    }
  for (auto& thread : threads)
    thread.join();
}

Possible output:

thread #1 is   0.00 % done
thread #0 is   0.00 % done
thread #0 is  26.84 % done
thread #0 is  76.15 % done
thread #3 is   0.00 % done
thread #0 has completed its work
thread #3 is  34.70 % done
thread #3 is  78.92 % done
thread #3 is  91.89 % done
thread #3 has completed its work
thread #1 is  26.98 % done
thread #1 is  73.84 % done
thread #1 has completed its work
thread #2 is   0.00 % done
thread #2 is  10.17 % done
thread #2 is  29.85 % done
thread #2 is  79.03 % done
thread #2 has completed its work
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!