Momenta后端C++实习面经,C++实现线程池
1、Linux内存管理 页表和虚拟地址 为什么要有页表 物理内存不够了怎么办
虚拟内存与地址空间
虚拟内存是一种内存管理技术,为进程提供一个抽象的、连续的内存空间,研究表明这允许进程感知的内存可能大于实际物理内存。
- 虚拟地址:进程使用的逻辑地址,例如程序中的指针值。它不是直接对应RAM的物理位置,而是通过页表映射。
- 物理地址:RAM中实际存储数据的地址,由内存管理单元(MMU)根据页表翻译得到。
比如说,两个进程可能都有虚拟地址0x1000,但映射到不同的物理地址,确保隔离。
Linux通过虚拟内存实现多进程并发,每个进程有独立地址空间(通常4GB在32位系统,64位系统更大),这由内核管理。
页表的作用与结构
页表是操作系统维护的数据结构,用于存储虚拟地址到物理地址的映射。
- 结构:在x86架构中,页表是多级结构(如PML4、PDPT、PD、PT),每个页面通常4KB。
- 功能:
- 地址翻译:MMU使用页表将虚拟地址分解为页号和偏移,查找物理页框。
- 内存保护:页表条目包含权限(如读/写/执行),防止进程访问非法内存。
- 内存共享:多个进程可映射同一物理页,如共享库。
- 按需分页:仅在需要时加载页面到内存,优化资源。
为什么需要页表
- 内存保护:确保进程只能访问自己内存,防止崩溃或攻击。例如,进程A不能读写进程B的内存。
- 内存共享:如多个进程共享同一程序代码(如libc),减少内存占用。
- 地址空间隔离:每个进程有独立地址空间,简化编程。
- 高效内存利用:通过按需分页和页面置换,允许运行超过物理内存的进程。
- 硬件支持:现代CPU依赖MMU和页表实现虚拟内存,Linux充分利用此功能。
物理内存不足时的处理
当RAM(物理内存)不足,Linux使用交换机制:
- 交换空间:可以是专用分区或文件,扩展内存容量。
- 分页:虚拟内存分为固定大小的页面(通常4KB),当进程访问不在内存的页面,触发页面错误(page fault)。
- 页面置换:若RAM满,内核选择一个页面移出,写入交换空间,释放内存。例如,使用LRU(最近最少使用)算法选择牺牲页面。
- 过程:
- 进程访问虚拟地址,MMU发现页面不在RAM,触发页面错误。
- 内核检查页面是否在交换空间,若是,从磁盘读入RAM,更新页表。
- 若RAM无空闲,选一个页面(可能脏页)写入交换区,腾出空间。
- 完成加载,进程继续运行。
- 性能影响:磁盘I/O远慢于RAM,频繁交换可能导致“抖动”,系统更多时间处理页面而非执行任务,性能下降。
2、每个进程的地址空间同样大吗?
进程地址空间是指进程可以访问的虚拟内存范围,它由操作系统管理,不是直接对应物理内存。每个进程有自己的地址空间,确保隔离和安全。
在32位系统上,每个进程的地址空间通常为4GB,但操作系统可能分给用户空间2-3GB,剩余给内核。
在64位系统上,Windows通常给每个进程8TB用户地址空间,Linux可能高达128TB,但实际可用范围受配置限制。
给个表帮助理解
| 系统/架构 | 地址空间大小 | 用户空间 | 内核空间 | 备注 |
|---|---|---|---|---|
| 32位Windows | 4GB | 2GB | 2GB | 可配置 |
| 32位Linux | 4GB | 2-3GB | 1-2GB | 配置依赖 |
| 64位Windows | 16EB(理论) | 8TB | 8TB+ | 用户模式实际8TB |
| 64位Linux | 128TB(用户) | 128TB | 剩余 | 配置可能限制 |
3、OOP三特性 C++多态(静态和动态) ?
面向对象编程的三大特性
封装
封装是将数据和操作数据的函数绑定在一起,隐藏内部实现,只暴露必要的接口。例如,银行账户类可以隐藏余额,只提供存款和取款方法,保护数据安全。
继承
继承允许一个类从另一个类继承属性和方法,促进代码复用。例如,储蓄账户可以继承银行账户的基本功能,并添加利息计算。
多态
多态让不同类的对象能以相同方式处理,分为静态多态(编译时)和动态多态(运行时)。
静态多态
编译时多态,编译器根据参数类型决定调用哪个函数。
实现方式:
- 函数重载:同名函数,参数列表不同。
void print(int num) {
std::cout << "Integer: " << num << std::endl;
}
void print(double num) {
std::cout << "Double: " << num << std::endl;
}
int main() {
print(5); // 调用print(int)
print(3.14); // 调用print(double)
return 0;
}- 模板:泛型编程,编译时生成特定类型版本。
template<typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
int main() {
std::cout << max(5, 10) << std::endl; // max<int>
std::cout << max(3.5, 2.8) << std::endl; // max<double>
return 0;
}动态多态
运行时多态,根据对象实际类型决定调用哪个函数。
实现方式:通过虚函数和继承。
- 基类声明虚函数(virtual),派生类可重写(override)。
- 使用基类指针或引用调用,运行时通过虚表(vtable)动态绑定。
class Shape {
public:
virtual void draw() = 0; // 纯虚函数
virtual ~Shape() {} // 虚析构函数
};
class Circle : public Shape {
public:
void draw() override {
std::cout << "Drawing a circle" << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() override {
std::cout << "Drawing a rectangle" << std::endl;
}
};
int main() {
Shape* shapes[] = {new Circle(), new Rectangle()};
for (auto shape : shapes) {
shape->draw(); // 运行时调用正确版本
}
for (auto shape : shapes) {
delete shape;
}
return 0;
}对比表
| 特性 | 静态多态 | 动态多态 |
|---|---|---|
| 实现方式 | 函数重载、模板 | 虚函数、继承 |
| 绑定时间 | 编译时 | 运行时 |
| 性能 | 高,无运行时开销 | 低,虚表查找有开销 |
| 灵活性 | 低,类型需编译时确定 | 高,适合运行时类型变化 |
| 典型应用 | 数学库、通用算法 | 图形库、事件系统 |
| 代码复杂度 | 简单,模板可能膨胀 | 复杂,需管理虚函数和继承 |
4、函数重载可以仅返回值不同吗 为什么不可以?
函数重载不能仅基于返回值类型不同。
C++中,函数重载主要看函数名和参数列表(包括参数类型和数量),返回值类型不被考虑。如果两个函数有相同的名字和参数列表,但返回值不同,编译器会报错,因为它无法根据调用时的参数决定使用哪个函数。例如,int func()和double func()会引发编译错误,因为参数列表完全相同,编译器无法区分。
原因是,函数调用时我们提供参数,编译器根据参数匹配函数,而返回值类型在调用时并不参与匹配。所以,C++不允许仅靠返回值类型来重载函数,以避免歧义。
5、多进程 多线程 线程池区别 使用场景举例?
多进程
一个应用由多个进程组成,每个进程有独立的内存空间,通过进程间通信(IPC)如管道、套接字或共享内存交换数据。
特点
- 隔离性:进程独立,崩溃不影响其他进程。
- 资源管理:每个进程有自己的代码段、数据段、堆和栈,内存开销大。
- 通信:通过IPC,效率较低,如管道、消息队列。
- 创建开销:启动新进程(如fork())较慢,涉及内存复制。
优缺点
- 优点:
- 高稳定性,适合需要隔离的任务。
- 可跨机器分布,扩展性强。
- 缺点:
- 通信开销高,资源消耗大。
- 进程切换开销高,影响性能。
多线程
在一个进程内运行多个线程,所有线程共享同一内存空间,通过共享变量通信,但需同步避免竞争。
特点
- 依赖性:线程依赖进程,共享代码段、数据段和堆,私有栈。
- 通信:通过共享内存,效率高,但需锁(如std::mutex)同步。
- 创建开销:启动线程(如std::thread)较快,适合频繁创建。
- 稳定性:线程崩溃可能影响整个进程。
优缺点
- 优点:
- 通信快,适合共享数据场景。
- 创建和切换开销低,性能优。
- 缺点:
- 同步复杂,易引发竞争条件。
- 线程错误可能导致进程崩溃。
线程池
一种线程管理机制,预创建一组线程,复用执行任务队列中的任务,减少创建销毁开销。
特点
- 资源管理:固定线程数,任务通过队列分配,防止过载。
- 效率:避免频繁创建线程,适合短任务。
- 同步:任务需独立或同步,共享内存需锁。
- 扩展性:池大小可调,适应负载变化。
优缺点
- 优点:
- 高效管理线程,减少开销。
- 适合高并发短任务,负载均衡。
- 缺点:
- 实现复杂,需管理队列和分配。
- 不适合长任务,池可能阻塞。
对比表
| 特性 | 多进程 | 多线程 | 线程池 |
|---|---|---|---|
| 隔离性 | 高,进程独立 | 低,线程共享进程 | 低,池内线程共享内存 |
| 通信效率 | 低,需IPC | 高,共享内存 | 高,池内共享 |
| 创建开销 | 高,进程启动慢 | 低,线程创建快 | 低,复用线程 |
| 稳定性 | 高,崩溃不影响其他 | 低,线程错影响进程 | 低,池内错误影响进程 |
| 适用场景 | 服务器、批处理 | UI、计算任务 | Web服务器、短任务 |
| 示例 | Apache Prefork | 浏览器渲染 | Nginx请求处理 |
6、操作系统堆和栈的区别 二者的大小?
区别
- 堆:程序运行时动态分配的内存区域,由程序员通过new(C++)或malloc(C)申请,使用delete或free释放。
- 栈:自动管理的内存区域,遵循“后进先出”(LIFO)原则,由编译器分配和释放,主要存储局部变量和函数调用信息。
管理方式
堆需要程序员显式分配和释放,容易出现内存泄漏;栈由系统自动处理,效率高但大小受限。
大小
- 堆:堆初始小,可根据需要增长,最大受限于可用物理内存和交换空间。
- 栈:栈大小通常由操作系统或编译器设定,每个线程有独立栈。
堆可以根据需要增长,通常受系统内存限制;栈大小固定,Linux默认线程栈为8MB,Windows可能为1MB(GUI)或256KB(控制台)。
区别表
| 特性 | 堆(Heap) | 栈(Stack) |
|---|---|---|
| 管理方式 | 手动(new/delete) | 自动(编译器) |
| 分配速度 | 较慢(操作系统查找内存块) | 很快(调整栈指针) |
| 生命周期 | 程序员控制,存活至释放 | 作用域结束自动释放 |
| 内存布局 | 非连续,易碎片 | 连续,无碎片 |
| 大小限制 | 较大(受物理内存限制) | 较小(OS设定,如8MB) |
| 线程安全 | 需同步(如锁) | 线程私有,天然安全 |
| 溢出风险 | 内存不足(分配失败) | 栈溢出(递归或大变量) |
| 典型用途 | 动态对象、共享资源 | 局部变量、调用帧 |
7、C++实现线程池?
线程池是一种线程管理模式,预先创建固定数量的线程,任务通过队列分配给空闲线程执行。
线程池的主要作用是:
- 减少线程创建和销毁的开销,适合短任务频繁执行。
- 控制并发数量,防止系统过载。
- 提高资源利用率,优化性能。
实现线程池的步骤:
- 任务队列:存储待执行的任务,通常用std::queue或std::deque,每个任务可以是函数对象(如std::function)。
- 线程管理:预创建一组线程,循环从任务队列中取任务执行。
- 同步机制:使用条件变量(std::condition_variable)和互斥锁(std::mutex)确保线程安全,防止竞争条件。
- 任务提交和执行:提供接口提交任务,线程池调度执行。
#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <vector>
class ThreadPool {
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queue_mutex;
std::condition_variable condition;
bool stop;
public:
ThreadPool(size_t num_threads) : stop(false) {
// 创建num_threads个线程
for (size_t i = 0; i < num_threads; ++i) {
workers.emplace_back([this]() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queue_mutex);
condition.wait(lock, [this]() { return stop || !tasks.empty(); });
if (stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task(); // 执行任务
}
});
}
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
condition.notify_all();
for (auto& worker : workers) {
worker.join();
}
}
// 提交任务
template<typename F>
void enqueue(F&& f) {
{
std::unique_lock<std::mutex> lock(queue_mutex);
tasks.emplace(std::forward<F>(f));
}
condition.notify_one();
}
};
int main() {
ThreadPool pool(4); // 创建4个线程的线程池
for (int i = 0; i < 8; ++i) {
pool.enqueue([i]() {
std::cout << "Task " << i << " executed by thread " << std::this_thread::get_id() << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟工作
});
}
std::this_thread::sleep_for(std::chrono::seconds(1)); // 等待任务完成
return 0;
}