【发布时间】:2020-08-17 16:02:30
【问题描述】:
我一直在研究 Windows 和 Linux (Debian) 中一些 C++ REST API 框架的内存使用情况。特别是,我研究了这两个框架:cpprestsdk 和 cpp-httplib。在这两者中,都会创建一个线程池并用于为请求提供服务。
我从cpp-httplib 获取线程池实现并将其放在下面的最小工作示例中,以显示我在 Windows 和 Linux 上观察到的内存使用情况。
#include <cassert>
#include <condition_variable>
#include <functional>
#include <iostream>
#include <list>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
using namespace std;
// TaskQueue and ThreadPool taken from https://github.com/yhirose/cpp-httplib
class TaskQueue {
public:
TaskQueue() = default;
virtual ~TaskQueue() = default;
virtual void enqueue(std::function<void()> fn) = 0;
virtual void shutdown() = 0;
virtual void on_idle() {};
};
class ThreadPool : public TaskQueue {
public:
explicit ThreadPool(size_t n) : shutdown_(false) {
while (n) {
threads_.emplace_back(worker(*this));
cout << "Thread number " << threads_.size() + 1 << " has ID " << threads_.back().get_id() << endl;
n--;
}
}
ThreadPool(const ThreadPool&) = delete;
~ThreadPool() override = default;
void enqueue(std::function<void()> fn) override {
std::unique_lock<std::mutex> lock(mutex_);
jobs_.push_back(fn);
cond_.notify_one();
}
void shutdown() override {
// Stop all worker threads...
{
std::unique_lock<std::mutex> lock(mutex_);
shutdown_ = true;
}
cond_.notify_all();
// Join...
for (auto& t : threads_) {
t.join();
}
}
private:
struct worker {
explicit worker(ThreadPool& pool) : pool_(pool) {}
void operator()() {
for (;;) {
std::function<void()> fn;
{
std::unique_lock<std::mutex> lock(pool_.mutex_);
pool_.cond_.wait(
lock, [&] { return !pool_.jobs_.empty() || pool_.shutdown_; });
if (pool_.shutdown_ && pool_.jobs_.empty()) { break; }
fn = pool_.jobs_.front();
pool_.jobs_.pop_front();
}
assert(true == static_cast<bool>(fn));
fn();
}
}
ThreadPool& pool_;
};
friend struct worker;
std::vector<std::thread> threads_;
std::list<std::function<void()>> jobs_;
bool shutdown_;
std::condition_variable cond_;
std::mutex mutex_;
};
// MWE
class ContainerWrapper {
public:
~ContainerWrapper() {
cout << "Destructor: data map is of size " << data.size() << endl;
}
map<pair<string, string>, double> data;
};
void handle_post() {
cout << "Start adding data, thread ID: " << std::this_thread::get_id() << endl;
ContainerWrapper cw;
for (size_t i = 0; i < 5000; ++i) {
string date = "2020-08-11";
string id = "xxxxx_" + std::to_string(i);
double value = 1.5;
cw.data[make_pair(date, id)] = value;
}
cout << "Data map is now of size " << cw.data.size() << endl;
unsigned pause = 3;
cout << "Sleep for " << pause << " seconds." << endl;
std::this_thread::sleep_for(std::chrono::seconds(pause));
}
int main(int argc, char* argv[]) {
cout << "ID of main thread: " << std::this_thread::get_id() << endl;
std::unique_ptr<TaskQueue> task_queue(new ThreadPool(40));
for (size_t i = 0; i < 50; ++i) {
cout << "Add task number: " << i + 1 << endl;
task_queue->enqueue([]() { handle_post(); });
// Sleep enough time for the task to finish.
std::this_thread::sleep_for(std::chrono::seconds(5));
}
task_queue->shutdown();
return 0;
}
当我运行这个 MWE 并查看 Windows 与 Linux 中的内存消耗时,我得到了下图。对于 Windows,我使用 perfmon 来获取 Private Bytes 值。在 Linux 中,我使用docker stats --no-stream --format "{{.MemUsage}} 来记录容器的内存使用情况。这与res 在容器内运行的来自top 的进程一致。从图中可以看出,当线程在 handle_post 函数中为 Windows 中的 map 变量分配内存时,当函数在下一次调用该函数之前退出时,内存被归还 .这是我天真地期待的行为类型。我没有关于操作系统如何处理线程保持活动状态时在线程中执行的函数分配的内存的经验,例如在线程池中。在 Linux 上,内存使用量似乎在不断增长,并且在函数退出时内存没有归还。当所有 40 个线程都已使用,并且还有 10 个任务要处理时,内存使用量似乎停止增长。有人可以从内存管理的角度对 Linux 中正在发生的事情给出一个高层次的看法,甚至可以提供一些关于在哪里可以找到关于这个特定主题的背景信息的一些指针?
编辑 1:我已编辑下图以显示在 Linux 容器中每秒运行 ps -p <pid> -h -o etimes,pid,rss,vsz 时 rss 的输出值,其中 <pid> 是正在运行的进程的 id测试。与docker stats --no-stream --format "{{.MemUsage}}的输出有合理的一致性。
编辑 2:根据下面关于 STL 分配器的评论,我通过将 handle_post 函数替换为以下内容并添加包含 #include <cstdlib> 和 #include <cstring>,从 MWE 中删除了映射。现在,handle_post 函数只是为 500K ints 分配和设置内存,大约是 2MiB。
void handle_post() {
size_t chunk = 500000 * sizeof(int);
if (int* p = (int*)malloc(chunk)) {
memset(p, 1, chunk);
cout << "Allocated and used " << chunk << " bytes, thread ID: " << this_thread::get_id() << endl;
cout << "Memory address: " << p << endl;
unsigned pause = 3;
cout << "Sleep for " << pause << " seconds." << endl;
this_thread::sleep_for(chrono::seconds(pause));
free(p);
}
}
我在这里得到相同的行为。在示例中,我将线程数减少到 8 个,任务数减少到 10 个。下图显示了结果。
编辑 3:我添加了在 Linux CentOS 机器上运行的结果。它与 Debian docker 镜像结果的结果大体一致。
编辑 4:根据下面的另一条评论,我在 valgrind 的 massif 工具下运行了示例。 massif 命令行参数在下图中。我用--pages-as-heap=yes 运行它,下面的第二张图片,没有这个标志,下面的第一张图片。第一张图片表明~2MiB 内存分配给(共享)堆,因为handle_post 函数在线程上执行,然后在函数退出时释放。这是我所期望的,也是我在 Windows 上观察到的。我还不确定如何用--pages-as-heap=yes 解释图表,即第二张图片。
我无法将第一张图像中massif 的输出与上图中显示的ps 命令中的rss 的值相一致。如果我运行 Docker 映像并使用 docker run --rm -it --privileged --memory="12m" --memory-swap="12m" --name=mwe_test cpp_testing:1.0 将容器内存限制为 12MB,则容器在第 7 次分配时内存不足并被操作系统杀死。我在输出中得到Killed,当我查看dmesg 时,我看到Killed process 25709 (cpp_testing) total-vm:529960kB, anon-rss:10268kB, file-rss:2904kB, shmem-rss:0kB。这表明来自ps 的rss 值准确地反映了进程正在使用的(堆)内存实际,而massif 工具正在计算它应该 em> 基于malloc/new 和free/delete 调用。这只是我从这个测试中得到的基本假设。我的问题仍然存在,即为什么,或者看起来,当handle_post 函数退出时堆内存没有被释放或释放?
编辑 5:当您将线程池中的线程数从 1 增加到 4 时,我在下面添加了内存使用图。随着您增加线程数,该模式继续到 10,所以我没有包括 5 到 10。请注意,我在 main 的开头添加了 5 秒的暂停,这是图表中前约 5 秒的初始平线。看来,无论线程数如何,在处理第一个任务后都会释放内存,但在任务 2 到 10 之后没有释放内存(保留以供重用?)。这可能表明在处理期间调整了某些内存分配参数任务 1 执行(只是大声思考!)?
编辑6:根据详细答案below的建议,我在运行示例之前将环境变量MALLOC_ARENA_MAX设置为1和2。这给出了下图中的输出。根据答案中给出的对这个变量的影响的解释,这是预期的。
【问题讨论】:
-
我首先想到的一个操作系统差异是 Windows 的默认堆栈大小为 1 MB。 linux它更大。我相信 8 到 10MB。
-
@drescherjm 谢谢。如果我将
handle_post中添加到地图的元素数量从 5K 增加到 50K,则 Linux 行在扁平化之前线性增长到 ~140 MB,而 Windows 行在处理每个任务后保持在 ~13 MB 并下降。这让我感到困惑。即使在函数退出时调用了包装映射的结构的析构函数,分配的内存似乎仍然在 Linux 上发挥作用。同样,我在这里的经验很少,所以可能只是有一个非常基本的误解。 -
你激起了我的好奇心:我查找了
MemUsage对docker stats的含义,我只能找到:docs.docker.com/engine/reference/commandline/stats 没有帮助。从字面上看,有几十种不同的方法来衡量一个进程“使用”了多少“内存”,具体取决于 Windows 和 Linux 版本的 docker 报告的“内存使用情况”。您可能会将苹果与建筑进行比较。 -
@SolomonSlow 感谢您的关注。
docker stats输出与在容器内运行top并查看res列的输出一致。res的top值稍微低一些,因为它只是被监控的进程本身。我可以编辑问题并使用此结果更新图表。我可以访问运行 CentOS 的 Linux 服务器,因此我也可以从那里添加结果。使用这些统计信息更新问题可能很有用。 -
假设没有内存泄漏(检查 valgrind),我已经看到实现提供的 STL 容器分配器会发生这种情况。它可能会分配一些空间,除非需要,否则不会将其归还。对于踢球和咯咯笑,尝试比较调试/发布版本。在操作系统之前,我会怀疑 STL、docker 和 malloc,尽管我承认我不知道是否存在 Linux 对回收进程释放的内存如此漠不关心的情况。
标签: c++ multithreading memory-management