запишете списък с променливи аргументи за извиквания на fprintf

Пиша тежка многонишкова [>170 нишки] c++11 програма. Всяка нишка регистрира информация в един файл, използван от всички нишки. От съображения за производителност искам да създам нишка log, която записва информацията чрез fprintf() в глобалния файл. Нямам идея как да организирам структурата, в която нишките worker записват информацията, която след това може да бъде прочетена от нишката log.

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

Друга причина за log нишката е, че трябва да „групирам“ изхода, в противен случай информацията от всяка работна нишка няма да бъде в блок:

грешно:

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

За да постигна това групиране, трябва да пазя изхода във всяка работна нишка, което забавя времето за изпълнение на нишката.

Как мога да запазя 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. да го отпечатате в буфер в worker. 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
Моля, дайте ми няколко дни, за да го проверя. Аз съм начинаещ в C++ и това е много нов код за мен... - person Peter VARGA; 21.02.2015
comment
Приех отговора ви, защото работи толкова далеч. За мен не можах да го използвам [Не използвам библиотеката boost; Имам нужда от разделител за хиляди, който прави форматирането по-сложно, ..]. Сега написах своя собствена версия. - person Peter VARGA; 28.02.2015