save variable argument list for fprintf calls

五迷三道 提交于 2019-12-20 03:34:10

问题


I am writing a heavy multi threaded [>170 threads] c++11 program. Each thread is logging information into one file used by all threads. For performance reasons I want to create a log thread which is writing the information via fprintf() into the global file. I have no idea how to organize the structure into which the worker threads are writing the information which can be then read by the log thread.

Why do I not call sprintf() in each worker thread and then just provide the output buffer to the log thread? For the formatted output into the log file I am using a locale in the fprintf() functions which is different than in the rest of the thread. Therefore I would have to switch and lock/guard permanently the xprintf() calls in order to differ the locale output. In the log thread I have one locale setting used for the whole output while the worker threads have their locale version.

Another reason for the log thread is that I have to "group" the output otherwise the information from each worker thread would not be in a block:

Wrong:

Information A Thread #1
Information A Thread #2
Information B Thread #1
Information B Thread #2

Correct:

Information A Thread #1
Information B Thread #1
Information A Thread #2
Information B Thread #2

In order to achieve this grouping I have to guard the output in each worker thread which is slowing the thread execution time.

How can I save the va_list into a structure that way it can be read by the log thread and passed back to fprintf()?


回答1:


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


来源:https://stackoverflow.com/questions/28640260/save-variable-argument-list-for-fprintf-calls

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!