使用内存调试工具:许多编程语言和开发环境提供了内存调试工具,例如Valgrind(用于C/C++)、Memory Profiler(用于Python)、Xcode Instruments(用于iOS/macOS开发)等。这些工具可以帮助检测内存泄漏并提供详细的报告,包括泄漏的位置和分配堆栈。 手动代码审查:仔细检查程序中的内存分配和释放代码。确保每个 malloc
、new
、calloc
或类似的函数都有对应的释放操作(free
、delete
、free
等)。记录内存分配和释放:在程序中记录每个内存分配和释放操作,以便跟踪资源的生命周期。一旦发现未释放的资源,您可以确定在何处分配了该资源。 使用智能指针:如果您使用支持智能指针的编程语言(如C++的 std::shared_ptr
或std::unique_ptr
),可以使用它们来管理内存,以减少手动内存管理的错误。静态代码分析工具:某些静态代码分析工具(例如Clang的 scan-build
)可以检测到潜在的内存泄漏问题,并提供警告。内存分析工具:使用专门的内存分析工具,例如Heap Profiler,可以可视化地查看程序的内存使用情况,从而找出泄漏。 测试用例:编写专门的测试用例来模拟程序的使用情况,以查找内存泄漏。这些测试用例可以捕获内存分配和释放的问题。 增量检查:您可以在程序的不同阶段进行内存检查,以查看哪个阶段导致了内存泄漏。 记录内存使用情况:记录程序的内存使用情况,以便可以比较不同版本之间的内存占用情况,以找出是否存在泄漏。
性能下降:当程序占用大量内存时,操作系统可能不得不频繁地进行内存页面交换,将内存中的数据写入磁盘并重新加载数据,这会导致程序的性能大幅下降。系统变得响应迟钝,运行速度减慢,用户体验变差。 程序崩溃:当系统的物理内存耗尽,无法满足程序的内存需求时,程序可能会崩溃或被操作系统强制终止。这通常是因为操作系统无法为程序分配所需的内存空间。 资源争夺:如果多个进程或线程同时竞争有限的内存资源,可能会导致资源争夺。这可能会导致死锁或无法预测的行为。 系统稳定性问题:内存过高可能会导致操作系统本身出现问题。操作系统需要维护内核数据结构和进程管理,如果内存不足,操作系统可能无法正常工作,导致整个系统不稳定。
空闲存储空间以空闲链表的方式组织(地址递增),每个块包含一个长度、一个指向下一块的指针以及一个指向自身存储空间的指针。( 因为程序中的某些地方可能不通过malloc调用申请,因此malloc管理的空间不一定连续。) 当有申请请求时,malloc会扫描空闲链表,直到找到一个足够大的块为止(首次适应)(因此每次调用malloc时并不是花费了完全相同的时间)。 如果该块恰好与请求的大小相符,则将其从链表中移走并返回给用户。如果该块太大,则将其分为两部分,尾部的部分分给用户,剩下的部分留在空闲链表中(更改头部信息)。因此malloc分配的是一块连续的内存。 释放时,首先搜索空闲链表,找到可以插入被释放块的合适位置。如果与被释放块相邻的任一边是一个空闲块,则将这两个块合为一个更大的块,以减少内存碎片。
线程局部缓存:tcmalloc 使用了一种线程局部缓存的机制,每个线程都有自己的内存缓存,这减少了多线程应用程序中的锁竞争,从而提高了性能。 高性能:tcmalloc 专注于提供高性能的内存分配和释放操作。它的设计目标是降低内存管理开销,减少内存分配的时间,特别是在多线程环境中。 内存利用率:tcmalloc 通过多种技术,如大小分类、中心缓存等,来提高内存利用率。它可以有效地减少内存碎片问题。 调试支持:tcmalloc 提供了一些调试工具和环境变量,用于帮助诊断和解决内存分配问题。 跨平台性:tcmalloc 可以在多种操作系统上使用,并且与各种编译器兼容。
std::vector<int> vec(100);
std::vector<int> vec;vec.reserve(1000); // 预留容量为 1000
std::vector的扩容行为并不直接暴露给用户,所以你不能直接查看它的扩容次数或扩容了多少。
std::vector扩容的情况。
容量信息:你可以使用 capacity()
成员函数来查看当前std::vector
的容量,即分配的内存空间大小。容量不等于元素数量,它表示在不需要重新分配内存的情况下,可以存储的元素数量。如果容量小于元素数量,那么std::vector
可能已经进行了一次或多次扩容。实验性分析:你可以通过编写一些测试代码来观察 std::vector
的扩容行为。例如,你可以在循环中不断地添加元素,并在每次添加后检查容量的变化。这样可以粗略地估计扩容的规模。
#include <iostream>#include <vector>int main() {std::vector<int> vec;size_t prevCapacity = vec.capacity();for (int i = 0; i < 1000; ++i) {vec.push_back(i);if (vec.capacity() != prevCapacity) {std::cout << "Capacity increased from " << prevCapacity << " to " << vec.capacity() << std::endl;prevCapacity = vec.capacity();}}return 0;}
int&& rvalue_ref = 42; // 右值引用
移动语义(Move Semantics):右值引用最常见的应用是在容器类如std::vector和std::string等的内部实现中,它们可以利用移动语义来高效地管理内存资源。通过将资源的所有权从一个对象(右值)转移到另一个对象,可以避免不必要的内存拷贝,提高性能。
std::vector<int> source = getLargeData(); // 获取一个大型数据std::vector<int> destination = std::move(source); // 使用移动语义,避免拷贝
完美转发(Perfect Forwarding):右值引用还用于实现完美转发,这是一种允许函数将参数原封不动地传递给其他函数的机制。这对于编写泛型代码和函数包装器非常有用。
template <typename Func, typename... Args>auto wrapper(Func&& func, Args&&... args) {// 对参数进行处理或日志记录等操作return std::forward<Func>(func)(std::forward<Args>(args)...);}
临时对象(Temporary Objects):右值引用通常用于引用临时对象,即那些不具备持久性的、一次性的对象。这些对象通常产生于表达式求值过程中,如函数返回值、类型转换等。 std::move() 函数:std::move() 是一个用于将左值强制转换为右值引用的函数,它并不实际移动数据,只是改变了对数据的引用方式。这对于告诉编译器我们希望执行移动操作非常有用。
std::vector<int> source = getLargeData(); // 获取一个大型数据std::vector<int> destination = std::move(source); // 使用std::move()告诉编译器执行移动操作
co_await、
co_yield等关键字来实现。
协作式多任务:协程是协作式多任务的一种实现方式,不同于线程的抢占式多任务,它需要程序员显式地控制协程的切换。 状态保存:协程可以在函数中保存状态,包括局部变量、指令指针等,以便在恢复执行时继续之前的状态。 异步编程:协程非常适合异步编程,通过 co_await
关键字可以等待异步操作完成而不会阻塞线程。
co_await
:在协程中等待异步操作完成,暂停协程执行。co_yield
:将值传递给协程的调用者,并临时暂停协程执行,直到调用者请求继续。co_return
:返回值并结束协程的执行。co_begin
:标记协程的起始点。co_end
:标记协程的结束点。
#include <iostream>#include <experimental/coroutine>#include <chrono>using namespace std::chrono_literals;// 定义一个协程类型struct MyCoroutine {struct promise_type {MyCoroutine get_return_object() {return {};}std::experimental::suspend_never initial_suspend() {return {};}std::experimental::suspend_never final_suspend() noexcept {return {};}void unhandled_exception() {}void return_void() {}};// 协程函数void operator()() {std::cout << "Coroutine started." << std::endl;std::this_thread::sleep_for(2s);std::cout << "Coroutine resumed." << std::endl;}};// 启动协程MyCoroutine createCoroutine() {co_await std::experimental::suspend_never{};}int main() {MyCoroutine coroutine = createCoroutine();coroutine();std::cout << "Main thread continues." << std::endl;return 0;}
进程(Process):
独立性:进程是操作系统分配资源的最小单位。每个进程都有自己独立的内存空间和系统资源,进程之间的通信需要额外的机制(例如管道、消息队列、共享内存等)。 开销:创建和销毁进程的开销比较大,因为需要分配和回收独立的内存空间和资源。 并发性:进程之间的并发性较高,可以在多核处理器上并行执行,但进程间的切换开销也较高。
线程(Thread):
共享资源:线程是进程的一部分,多个线程共享同一进程的内存空间和资源。因此,线程之间的通信和数据共享相对容易。 开销:相对于进程,线程的创建和销毁开销较小,因为它们共享相同的地址空间。 并发性:线程之间的并发性较高,可以在多核处理器上并行执行,线程的切换开销相对较低。
协程(Coroutine):
独立性:协程是用户级别的执行单元,与线程相比,协程更加轻量级。多个协程运行在同一个线程内,共享线程的内存空间。 协作式多任务:协程是协作式多任务的一种实现,需要显式地让出执行权给其他协程,切换开销非常小。 状态保存:协程可以保存自己的状态,包括局部变量、指令指针等,以便在恢复执行时继续之前的状态。 通信:协程之间的通信相对简单,可以通过函数调用和消息传递来实现。
进程是操作系统分配资源的最小单位,线程是进程的一部分,而协程是用户级别的执行单元。 进程之间的通信需要额外的机制,线程可以共享进程内的资源,协程共享线程内的资源。 进程切换开销最高,线程次之,协程最低。 线程和协程更适合多核处理器上的并行执行,进程通常用于更大规模的并发。
线程状态:每个线程都有一个状态,常见的状态包括就绪态、运行态和阻塞态。 就绪态:表示线程已经准备好运行,只等待分配CPU时间。 运行态:表示线程正在CPU上执行指令。 阻塞态:表示线程被阻塞,等待某个事件(如I/O操作完成)。 调度队列:操作系统维护一个或多个调度队列,每个队列中包含了不同状态的线程。例如,就绪队列包含了所有处于就绪态的线程。 调度器:调度器是操作系统内核的一部分,它负责从就绪队列中选择一个线程分配CPU时间。 调度算法:调度器使用不同的调度算法来选择下一个运行的线程。常见的调度算法包括: 抢占式调度:在任何时刻,操作系统可以中断当前运行的线程,并将CPU分配给另一个线程。这种方式下,线程可以被强制挂起,以便其他线程执行。常见的抢占式调度算法包括优先级调度和时间片轮转调度。 非抢占式调度:线程只有在主动放弃CPU时,才会让出CPU给其他线程执行。这种方式下,线程自身控制着线程的切换时机。常见的非抢占式调度算法包括协作式多任务。 上下文切换:当调度器选择了下一个线程后,需要进行上下文切换。上下文切换是保存当前线程的执行状态,包括寄存器、程序计数器、栈指针等,然后加载下一个线程的执行状态。 时间片:某些调度算法(如时间片轮转调度)会为每个线程分配一个时间片,即一段固定的时间。线程在自己的时间片用完后,让出CPU给下一个线程。 优先级:线程可以分配不同的优先级,高优先级的线程会被优先执行。但要注意不要滥用线程优先级,以免导致低优先级线程饥饿问题。
先来先服务
最短作业优先
最短剩余时间优先
最高响应比优先算法
响应比 Rp= (等待时间+预计运行时间)/预计运行时间=周转时间/预计运行时间
每个作业随着在后备池等待时间的增长其响应比也不断增长,而且,预计运行时间越短的作业响应比增长越快。最高响应比优先算法在每次调度时选择响应比最高的作业投入运行,这种算法较好地适应了长短作业混合的系统,使得调度的性能指标趋于合理。
轮转法
最高优先级算法
多级反馈队列算法
最短进程优先
实时系统中的调度算法
创建协程:首先,需要创建多个协程任务。每个协程通常表示一个独立的执行单元,可以在其中执行特定的任务。 初始化调度器:如果是由用户空间的协程库进行管理,需要初始化协程调度器。这通常包括创建协程队列、分配资源等。 选择要执行的协程:协程调度器会选择一个协程来执行。选择的策略可以是先进先出(FIFO)、优先级、循环调度等,具体根据需求而定。 切换协程上下文:当一个协程被选中执行时,需要进行上下文切换,保存当前协程的状态(寄存器、栈等),并加载目标协程的状态。 执行协程任务:协程开始执行其任务。协程可以在任务执行中主动挂起(yield)或等待其他协程。 挂起协程:协程在执行过程中可以选择挂起,将控制权交还给调度器。挂起的协程的状态会被保存,以便稍后恢复执行。 恢复协程:当挂起的协程再次被调度时,调度器会恢复其状态,并从上次挂起的地方继续执行。 重复选择和切换:调度器会根据选定的调度策略,重复选择和切换不同的协程,直到所有协程都执行完毕或达到终止条件。
缓存系统:Redis最常见的用途之一是作为缓存系统。由于其数据存储在内存中,读取速度非常快,适用于缓存经常访问的数据,从而减轻后端数据库的负载。 会话存储:Redis可以用于存储用户会话数据,尤其对于需要快速访问和更新的应用程序非常有用,如Web应用程序的用户会话管理。 消息队列:Redis支持发布/订阅模式(Pub/Sub),可以用于构建消息队列系统。生产者将消息发布到频道,而消费者则通过订阅频道来接收消息。这对于构建实时应用、日志处理和任务队列非常有用。 计数器和排行榜:Redis支持原子操作,因此可以用于实现计数器,例如,统计网站上的页面访问次数。它还可以用于构建排行榜,例如,跟踪分数最高的用户。 地理位置服务:Redis的 GEO
数据结构支持地理位置数据的存储和查询。这对于构建地理位置服务和附近搜索非常有用。分布式锁:Redis可以用于实现分布式锁,帮助控制多个进程或服务对共享资源的访问,以避免竞态条件。 持久性数据存储:虽然Redis的主要数据存储是内存中的,但它支持将数据持久化到磁盘,以防止数据丢失。 实时统计和监控:Redis可以用于实时统计和监控,例如,跟踪在线用户数、查看服务器性能指标等。
发布者(Publisher):发布者是向Redis消息队列发送消息的客户端。发布者使用 PUBLISH
命令将消息发布到一个或多个频道(Channels)。消息可以是任何字符串,通常表示一个事件或数据更新。频道(Channel):频道是消息的发布和订阅的主题或分类。发布者将消息发布到一个或多个频道,而订阅者则通过指定频道来接收消息。频道的名称是字符串,例如:"news"、"chatroom"等。 订阅者(Subscriber):订阅者是等待接收消息的客户端。它们使用 SUBSCRIBE
命令来订阅一个或多个频道,然后开始等待消息的到达。订阅者可以随时取消订阅。消息传递:当发布者发布一条消息到某个频道时,所有订阅了该频道的订阅者都将收到相同的消息。这是一对多的消息传递方式,消息传递是异步的,即发布者不等待订阅者接收消息。 模式匹配:Redis支持通配符订阅,允许订阅者使用通配符来匹配多个频道。例如,可以使用 SUBSCRIBE news.*
来订阅以"news."开头的所有频道。消息持久性:默认情况下,Redis Pub/Sub是瞬态的,如果没有订阅者在监听消息,消息将被丢弃。但Redis提供了一种方式来将消息持久化,称为"消息订阅历史"(Message Retention)。通过配置,可以保留最近发布到频道的N条消息,以便新订阅者可以获取最新的消息历史。 消息订阅历史:通过配置Redis,可以启用消息订阅历史,这样新订阅者可以在订阅频道后立即获取一定数量的历史消息。这对于确保新加入的订阅者能够获取到关键的起始数据非常有用。 取消订阅:订阅者可以随时取消对频道的订阅,通过 UNSUBSCRIBE
命令取消对特定频道的订阅,或使用UNSUBSCRIBE *
取消对所有频道的订阅。
IDL(接口定义语言): gRPC使用IDL来定义服务的接口规范。IDL文件可以使用Protocol Buffers(protobufs)编写,它是一种轻量级且高效的二进制序列化格式。IDL文件定义了服务方法、消息类型和消息字段。 多语言支持: gRPC支持多种编程语言,包括C++, Java, Python, Go, Ruby等,这意味着您可以使用不同的语言来开发客户端和服务器,并它们可以相互通信。 HTTP/2协议: gRPC使用HTTP/2作为底层传输协议。HTTP/2相对于HTTP/1.1具有更好的性能,支持双向流、多路复用、头部压缩等特性,这些特性有助于减少延迟和提高效率。 双向流: gRPC允许客户端和服务器之间建立双向流,这意味着它们可以同时发送和接收数据。这对于实现实时通信非常有用。 基于消息: gRPC通信是基于消息的,客户端和服务器使用protobufs消息进行通信。这些消息可以包含请求和响应数据,它们具有严格定义的结构。 自动化的序列化和反序列化: gRPC框架提供自动生成的代码来处理消息的序列化和反序列化,使得在不同语言之间通信变得更加容易。 支持多种认证和安全性: gRPC支持各种认证和安全性机制,包括SSL/TLS、OAuth2等,以确保通信的安全性。 拦截器(Interceptors): gRPC允许您在客户端和服务器上定义拦截器,以添加各种功能,如身份验证、日志记录、错误处理等。 流控制: gRPC支持流控制,以避免过多的数据传输导致内存问题。
二进制分帧(Binary Framing): HTTP/2使用二进制格式来传输数据,而不是HTTP/1.1中的文本格式。所有的消息(请求和响应)都被拆分成多个帧(frames),每个帧都有特定的用途,如头部帧、数据帧等。 使用二进制格式带来了多方面的好处,包括更高效的解析和传输,以及更好的错误检测和纠正。 多路复用(Multiplexing): HTTP/2引入了多路复用机制,允许多个请求和响应在单个TCP连接上同时传输,而无需按顺序等待。 这消除了HTTP/1.1中的队头堵塞问题,可以更充分地利用网络带宽,从而提高页面加载速度。 头部压缩(Header Compression): 为了减小消息头的传输大小,HTTP/2使用了头部压缩算法(通常为HPACK)。 服务器和客户端会维护一个动态的头部表,用于存储已发送或接收的头部字段,减少重复的传输。 流(Stream): HTTP/2引入了流的概念,每个请求和响应都与一个唯一的流相关联。 流可以并行传输,通过帧的标识符关联,而不会相互干扰。这使得多个请求可以同时在单个连接上处理,提高了效率。 服务器推送(Server Push): HTTP/2允许服务器在客户端请求之前主动推送资源。服务器可以发送与请求相关的其他资源,以减少客户端的等待时间。 这特别有助于提高网页加载速度,因为服务器可以预测客户端需要哪些资源。 流量控制(Flow Control): HTTP/2包括流量控制机制,使得客户端和服务器可以控制对方发送数据的速率。 这有助于防止过多的数据传输导致网络拥塞,并提高了性能。 优先级(Priority): HTTP/2允许为流设置优先级,确保关键资源的加载优先级更高。客户端可以向服务器发送优先级信息,以指导服务器资源的传输顺序。
#include <iostream>#include <thread>#include <mutex>#include <condition_variable>std::mutex mtx;std::condition_variable cv;bool printAlphabet = true;void printAlphabets() {for (char c = 'A'; c <= 'Z'; ++c) {std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, [] { return printAlphabet; });std::cout << c;printAlphabet = false;cv.notify_one();}}void printNumbers() {for (int i = 1; i <= 26; ++i) {std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, [] { return !printAlphabet; });std::cout << i;printAlphabet = true;cv.notify_one();}}int main() {std::thread t1(printAlphabets);std::thread t2(printNumbers);t1.join();t2.join();std::cout << std::endl;return 0;}

cv和一个布尔变量
printAlphabet用来表示当前是打印字母还是数字。每个线程在打印完一个字符或数字后会等待条件满足,然后唤醒另一个线程并切换
printAlphabet的状态,以实现交替打印。
以上便是我对这些面经的整理以及问题的一些理解,都仅供参考。希望大家有收获,但别盲目记忆这些问题答案,更多地理解其背后原理,面试就不会发挥的差到哪儿去。
如果说,有哪方面不明白,或者有质疑,可以随时私我,也烦请您看看书,一切以书本为准。
文章转载自阿Q正砖,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




