сохранить список переменных аргументов для вызовов fprintf

Я пишу тяжелую многопоточную [> 170 потоков] программу С++ 11. Каждый поток записывает информацию в один файл, используемый всеми потоками. Из соображений производительности я хочу создать поток log, который записывает информацию через fprintf() в глобальный файл. Я понятия не имею, как организовать структуру, в которую потоки worker записывают информацию, которая затем может быть прочитана потоком log.

Почему я не вызываю sprintf() в каждом потоке worker, а затем просто предоставляю выходной буфер потоку log? Для форматированного вывода в файл журнала я использую locale в функциях fprintf(), которые отличаются от остальной части потока. Поэтому мне пришлось бы постоянно переключать и блокировать/защищать вызовы xprintf(), чтобы отличать вывод locale. В потоке log у меня есть одна настройка locale, используемая для всего вывода, в то время как потоки worker имеют свою версию locale.

Другая причина для потока log заключается в том, что мне нужно "группировать" вывод, иначе информация из каждого потока worker не была бы в блоке:

Неправильный:

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

Верный:

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

Чтобы добиться этой группировки, я должен защищать вывод в каждом потоке worker, что замедляет время выполнения потока.

Как я могу сохранить va_list в структуру, чтобы поток log мог прочитать его и передать обратно в fprintf()?


person Peter VARGA    schedule 20.02.2015    source источник
comment
Почему изменение локали должно быть проблемой?   -  person Deduplicator    schedule 21.02.2015
comment
Если вы заботитесь о производительности, зачем вам 170 потоков? Они все активны одновременно? У вас есть массивно-параллельный процессор?   -  person Cameron    schedule 21.02.2015
comment
Как насчет использования vfprintf и передачи va_list?   -  person nullptr    schedule 21.02.2015
comment
@Deduplicator: Хороший вопрос. ХОРОШО; это не проблема, но группировка вывода с lock_guard замедляет потоки.   -  person Peter VARGA    schedule 21.02.2015
comment
@Inspired: Но как мне сохранить этот va_list в структуру, чтобы я мог использовать его в потоке log?   -  person Peter VARGA    schedule 21.02.2015
comment
@Cameron: Нет, они не активны сразу. У меня есть пороговое значение 32 одновременно работающих потоков, и поэтому очень важно, чтобы поток мог завершиться очень быстро, чтобы разбудить другой поток. Информация журнала важна, но не одновременно с выполнением потока. Поэтому я хочу разделить время вычисления задачи и время регистрации, потому что для меня это время потрачено впустую.   -  person Peter VARGA    schedule 21.02.2015
comment
Единственный разумный способ сделать то, что я вижу, это: 1. напечатать его в буфер в воркере. 2. Зарегистрируйте его в логгере. Регистратор может применять любой тип группировки, который он хочет.   -  person Deduplicator    schedule 21.02.2015
comment
Сколько различных типов аргументов у вас есть? Если вам не нужно больше, чем int, double и, возможно, какая-то строка, std::vector из некоторых union (или что-то вроде boost::variant) может справиться с этой задачей.   -  person 5gon12eder    schedule 21.02.2015
comment
@ 5gon12eder: да, но как мне объединить различные параметры в список переменных аргументов, чтобы их можно было использовать в функции printf ()?   -  person Peter VARGA    schedule 21.02.2015
comment
Есть ли шанс избавиться от функциональности локали для каждого потока и просто использовать одну (неизменяемую) локаль для всех потоков? Думаю, это облегчило бы вам жизнь.   -  person Jeremy Friesner    schedule 21.02.2015


Ответы (1)


Я не понимаю, как это было бы легко сделать, используя устаревший C vprintf с va_lists. Поскольку вы хотите передавать вещи между потоками, рано или поздно вам нужно будет каким-то образом использовать кучу.

Ниже приведено решение, использующее Boost.Format. для форматирования и Boost.Variant для передачи параметров. Пример завершен и работает, если вы соедините следующие блоки кода по порядку. Если вы компилируете с помощью GCC, вам нужно передать флаг компоновщика -pthread. И, конечно же, вам также понадобятся две библиотеки Boost, которые, однако, предназначены только для заголовков. Вот заголовки, которые мы будем использовать.

#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>

Во-первых, нам нужен какой-то механизм для асинхронного выполнения некоторых задач, в данном случае вывод наших сообщений журнала. Поскольку концепция является общей, я использую для этого «абстрактный» базовый класс Spooler. Его код основан на докладе Херба Саттера «Программирование без блокировок (или жонглирование бритвенными лезвиями)» на CppCon 2014 (часть 1, часть 2). Я не буду вдаваться в подробности об этом коде, потому что это в основном строительные леса, не имеющие прямого отношения к вашему вопросу, и я предполагаю, что у вас уже есть эта часть функциональности. Мой Spooler использует std::list, защищенный std::mutex, в качестве очереди задач. Возможно, вместо этого стоит рассмотреть возможность использования структуры данных без блокировок.

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_);
  }
};

Теперь из Spooler мы получаем Logger, который (частно) наследует свои асинхронные возможности от Spooler и добавляет специальные функции ведения журнала. Он имеет только один функциональный член с именем log, который принимает в качестве параметров строку формата и ноль или более аргументов для форматирования в нее как std::vector из boost::variants.

К сожалению, это ограничивает нас фиксированным количеством типов, которые мы можем поддерживать, но это не должно быть большой проблемой, поскольку C printf также не поддерживает произвольные типы. Ради этого примера я использую только int и double, но вы можете расширить список с помощью указателей std::strings, void * или чего-то еще.

Функция log строит лямбда-выражение, которое создает объект boost::format, передает ему все аргументы, а затем записывает его в std::log или туда, куда вы хотите отправить отформатированное сообщение.

Конструктор boost::format имеет перегрузку, которая принимает строку формата и языковой стандарт. Возможно, вам будет интересно это, так как вы упомянули о настройке пользовательской локали в комментариях. Обычный конструктор принимает только один аргумент — строку формата.

Обратите внимание, как все форматирование и вывод выполняются в потоке спулера.

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));
  }
};

Это все, что нужно. Теперь мы можем использовать Logger, как в этом примере. Важно, чтобы все рабочие потоки были join() обработаны до того, как Logger будет уничтожен, иначе он не будет обрабатывать все сообщения.

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();
}

Возможный вывод:

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
person 5gon12eder    schedule 21.02.2015
comment
Пожалуйста, дайте мне несколько дней, чтобы проверить это. Я новичок в С++, и для меня это много нового кода... - person Peter VARGA; 21.02.2015
comment
Я принял ваш ответ, потому что он работает так далеко. Для меня я не мог его использовать [я не использую библиотеку boost; Мне нужен разделитель тысяч, который усложняет форматирование, ..]. Я написал сейчас свою собственную версию. - person Peter VARGA; 28.02.2015