保存 fprintf 调用的变量参数列表
save variable argument list for fprintf calls
我正在编写一个繁重的多线程 [>170 线程] c++11 程序。每个线程都将信息记录到所有线程使用的 one 文件中。出于性能原因,我想创建一个 log 线程,它通过 fprintf()
将信息写入全局文件。我不知道如何组织 worker 线程写入信息的结构,然后 log 线程可以读取这些信息。
为什么我不在每个 worker 线程中调用 sprintf()
然后只向 log 线程提供输出缓冲区?对于日志文件中的格式化输出,我在 fprintf()
函数中使用了 locale
,这与线程的其余部分不同。因此,我必须永久切换 lock/guard 调用 xprintf()
以区分 locale
输出。
在 log 线程中,我有一个 locale
设置用于整个输出,而 worker 线程有它们的 locale
版本.
log 线程的另一个原因是我必须 "group" 输出,否则来自每个 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()
?
我不知道如何使用带有 va_list
s 的遗留 C vprintf
轻松完成此操作。当你想在线程之间传递东西时,迟早你需要以某种方式使用堆。
下面是使用 Boost.Format for the formatting and Boost.Variant 进行参数传递的解决方案。如果您按顺序连接以下代码块,则该示例是完整的并且可以运行。如果使用 GCC 编译,则需要传递 -pthread
链接器标志。当然,您还需要 header-only 两个 Boost 库。这是我们将使用的headers。
#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>
首先,我们需要一些机制来异步执行一些任务,在这种情况下,打印我们的日志消息。由于这个概念是笼统的,我为此使用了一个“抽象”基数 class Spooler
。它的代码基于 Herb Sutter 在 CppCon 2014 (part 1, part 2) 上的演讲 “Lock-Free 编程(或杂耍剃刀刀片)”。我不会详细介绍这段代码,因为它主要是与您的问题没有直接关系的脚手架,我假设您已经具备了这部分功能。我的 Spooler
使用 std::list
受 std::mutex
保护作为任务 queue。考虑改用 lock-free 数据结构可能是值得的。
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
的函数成员,它以格式字符串和零个或多个参数作为参数,将其格式化为 boost::variant
的 std::vector
。
不幸的是,这将我们限制在我们可以支持的固定数量的类型上,但这应该不是一个大问题,因为 C printf
也不支持任意类型。为了这个例子,我只使用 int
和 double
但你可以用 std::string
s,void *
指针或你有什么扩展列表。
log
函数构造一个 lambda 表达式,该表达式创建一个 boost::format
object,为它提供所有参数,然后将其写入 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
。在 Logger
被破坏之前所有工作线程都被 join()
编辑是很重要的,否则它不会处理所有消息。
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
我正在编写一个繁重的多线程 [>170 线程] c++11 程序。每个线程都将信息记录到所有线程使用的 one 文件中。出于性能原因,我想创建一个 log 线程,它通过 fprintf()
将信息写入全局文件。我不知道如何组织 worker 线程写入信息的结构,然后 log 线程可以读取这些信息。
为什么我不在每个 worker 线程中调用 sprintf()
然后只向 log 线程提供输出缓冲区?对于日志文件中的格式化输出,我在 fprintf()
函数中使用了 locale
,这与线程的其余部分不同。因此,我必须永久切换 lock/guard 调用 xprintf()
以区分 locale
输出。
在 log 线程中,我有一个 locale
设置用于整个输出,而 worker 线程有它们的 locale
版本.
log 线程的另一个原因是我必须 "group" 输出,否则来自每个 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()
?
我不知道如何使用带有 va_list
s 的遗留 C vprintf
轻松完成此操作。当你想在线程之间传递东西时,迟早你需要以某种方式使用堆。
下面是使用 Boost.Format for the formatting and Boost.Variant 进行参数传递的解决方案。如果您按顺序连接以下代码块,则该示例是完整的并且可以运行。如果使用 GCC 编译,则需要传递 -pthread
链接器标志。当然,您还需要 header-only 两个 Boost 库。这是我们将使用的headers。
#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>
首先,我们需要一些机制来异步执行一些任务,在这种情况下,打印我们的日志消息。由于这个概念是笼统的,我为此使用了一个“抽象”基数 class Spooler
。它的代码基于 Herb Sutter 在 CppCon 2014 (part 1, part 2) 上的演讲 “Lock-Free 编程(或杂耍剃刀刀片)”。我不会详细介绍这段代码,因为它主要是与您的问题没有直接关系的脚手架,我假设您已经具备了这部分功能。我的 Spooler
使用 std::list
受 std::mutex
保护作为任务 queue。考虑改用 lock-free 数据结构可能是值得的。
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
的函数成员,它以格式字符串和零个或多个参数作为参数,将其格式化为 boost::variant
的 std::vector
。
不幸的是,这将我们限制在我们可以支持的固定数量的类型上,但这应该不是一个大问题,因为 C printf
也不支持任意类型。为了这个例子,我只使用 int
和 double
但你可以用 std::string
s,void *
指针或你有什么扩展列表。
log
函数构造一个 lambda 表达式,该表达式创建一个 boost::format
object,为它提供所有参数,然后将其写入 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
。在 Logger
被破坏之前所有工作线程都被 join()
编辑是很重要的,否则它不会处理所有消息。
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