目 录CONTENT

文章目录

性能部分

TalentQ
2025-08-28 / 0 评论 / 0 点赞 / 6 阅读 / 0 字

死锁产生的条件,如何避免

死锁需要同时满足4个条件:

互斥:某资源一次只能被一个线程占有,其他线程必须等待;

占有且等待:一个线程已经占有了某些资源,同时又请求其他资源,但被阻塞,导致占有的资源不释放。

不可抢占:已经分配的资源不能被强制抢占,只能由占有它的线程主动释放;

循环等待:若干线程形成一个紫云等待环,每个线程都在等待下一个线程持有的资源。

打破其中任何一个条件就能避免死锁:

破坏互斥:减少对资源的独占使用。如果可以让资源被多个线程共享,比如只读资源,则不会产生死锁。但实际中互斥往往是必须的。

破坏占有且等待:一次性生申请所有需要的资源。线程在进入临界区之前,一次性申请所有需要的资源,申请不到则释放已经占有的资源并重新尝试;

破坏不可抢占:支持资源抢占。当某线程请求资源被阻塞时,系统可以强制回收它已占有的资源,分配给其他线程;

破坏循环等待:统一资源申请顺序。规定所有线程按照相同的顺序申请资源,这样不会形成循环等待。例如,线程A和线程B都必须先申请资源1,再申请资源2,避免A拿1等2、B拿2等1的环形等待。

编程注意事项:

保持锁的申请顺序一致;尽量减少持锁时间;能用局部锁就不用全局锁;分析可能的资源依赖关系,避免形成闭环。

内存泄露如何检查,怎么处理

如何检查:

1 使用工具检测

ASAN(AddressSanitizer GCC/Clang)。编译时加上 -fsanitize=address,运行后会检测内存泄露和越界。

Valgrind,运行程序时使用 valgrind --leak-check=full ./your_program,它会报告未释放的内存块及分配的位置。

gdb:

2 手动检查代码

查找 new/malloc 等分配内存的地方,确认每个分配都正确对应 delete/free。注意代码证中异常处理时是否有提前return,要确保return前释放资源。

如何处理:

1 编码遵循RAII,使用智能指针管理动态内存;

2 在异常处理时,使用智能指针或局部对象,确保释放资源;

3 手动正确地匹配 new-delete、malloc-free;

4 在开发和测试阶段,定期使用工具检测内存泄露。

CPU负载过高如何排查原因,如何处理

这个问题非常值得单开一篇,这里先做简要回答。

先明确两个性能指标

  • CPU使用率(Usage):在一段时间内CPU 被占用的时间占总时间的比例,是一个百分比数值;

  • 平均负载(Load Average):单位时间内,系统处于可运行状态的进程数,包括正在运行和等待CPU的进程。当平均负载高于 CPU 数量 70% 的时候,就需要关注并分析负载高的问题了 。

如何排查:

使用top或htop,实时查看系统的Load Average(通常有三个数字,分别表示过去1 min、5min、15min)和各进程CPU占用情况,定位哪个进程占用过高。

如果用户态CPU过高,说明应用程序本身的逻辑占用了大量CPU,排查代码中是否有计算密集型任务(循环、加解密、复杂数学运算等)、低效代码(频繁创建销毁对象、重复计算等)、频繁锁竞争;

如果内核态CPU过高,说明应用程序频繁触发内核操作,排查系统调用(频繁读写小文件、频繁创建销毁线程/进程、频繁网络收发send/recv);

如果iowait过高,说明CPU等待io时间过长,排查磁盘io瓶颈(频繁读写大文件)、网络io瓶颈(带宽不足、远程API响应慢、频繁swap);

此外,可以使用性能分析器perf定位热点函数;检查日志是否有异常。

如何处理:

如果是进程/线程异常,则直接 sudo kill -9 PID 停掉即可,后续针对性优化代码;如果进程/线程正常,代码逻辑合理,那就应该升级硬件,提高系统自身的性能。

cache line在多线程中的问题

Cache line(缓存行)是CPU cache的基本存储单位,通常为 64 Bytes,每次CPU访问内存时,是以cache line 为单位进行加载和存储。当CPU访问某个内存地址时。会把该地址所在的整个cache line加载到cache里,即使只访问其中的一个字节。

多线程中,cache line 可能会引发相关性能问题:

False Sharing(伪共享)。

在多核处理器上,如果两个线程在不同的核上并行执行,它们操作不同的变量,但这些变量恰好位于同一个cache line内。由于缓存一致性协议(如MESI)需要维护缓存的一致性,导致线程之间频繁互相使缓存失效,从而严重影响性能。

举例来说,假设两个线程分别更新变量A和B,A和B在内存上是相邻的,且落在同一个cache line上,此时 线程1修改A,会导致cache line在线程2所在的核的缓存失效,线程2修改B,会导致cache line在线程1所在的核的缓存失效。

解决方法:

  • 填充(padding)。在变量之间插入填充字段,使得变量单独占据一个cache line;

  • 对齐(alignment)。使用编译指令alignas(64),或使用 linux 内核的宏 __cacheline_aligned_in_smp;

Cache Line Contention(缓存行争用)

多个线程即使读写的是同一个变量,如果频繁读写,也会让其它核的缓存频繁失效,导致频繁更新缓存。

解决方法:

减少共享变量;分段计数,最后再合并。

参考:CPU cache line,非常直观。

讲讲C++内存模型

这个问题非常值得单开一篇,这里先做简要回答。

在多线程环境下,CPU和编译器会对代码进行优化,可能导致指令重排序和缓存不一致。C++内存模型定义了线程间共享变量的访问规则,确保数据的可见性原子性有序性

原子操作:

C++引入 <atomic> 头文件,提供原子类型和操作,防止数据竞争。常用的类型包括 std::atomic<int>、std::atomic<bool>、std::atomic_flag 等。

内存序:

内存序控制原子操作在多线程环境下的可见性和顺序。

不同的内存序适用于不同的场景,选择合适的内存序有助于写出高效又安全的并发代码。

默认使用 memory_order_seq_cst (顺序一致性)最安全,但性能可能较低。追求更高性能时,可以选择更宽松的内存序,但需要确保不会引入竞态条件。

内存序类型

说明

memory_order_seq_cst

顺序一致性,最严格的顺序保证,所有线程看到的原子操作顺序一致(默认值)。

memory_order_relaxed

最宽松的顺序,不保证任何顺序,仅保证原子操作本身的原子性。

memory_order_acquire

获取语义,保证该操作之后的所有读写不会被重排到该操作之前。

memory_order_release

释放语义,保证该操作之前的所有读写不会被重排到该操作之后。

memory_order_acq_rel

获取+释放语义,结合了 acquire 和 release 的效果。

在生产者-消费者模型中通常用 memory_order_release/acquire:

#include <atomic>
#include <iostream>
#include <thread>

std::atomic<int> data{0};
std::atomic<bool> ready{false};

void producer() {
  data.store(42, std::memory_order_relaxed);  // 数据写入,无序约束
  ready.store(true,
              std::memory_order_release);  // 释放,确保 data 写入先于 ready
}

void consumer() {
  while (!ready.load(
      std::memory_order_acquire)) {  // 获取,确保 ready 读取后再读取data
                                     // 自旋等待
  }
  std::cout << "data: " << data.load(std::memory_order_relaxed) << std::endl;
}

int main() {
  std::thread prod(producer);
  std::thread cons(consumer);

  prod.join();
  cons.join();
  return 0;
}

什么是线程安全,如何保证

什么是线程安全

线程安全是一个关于代码在并发环境中行为的属性。如果一个函数、类或数据结构是线程安全的,那么当多个线程同时访问它时,无论操作系统如何调度这些线程的交错执行,它都能表现出正确的行为,并且不需要调用方做任何额外的同步。

常见的非线程安全场景包括:

  • 数据竞争:多个线程同时读写同一个变量,且至少有一个是写操作。

  • 竞态条件:程序的正确性依赖于线程操作的执行时序。即使没有数据竞争,也可能因为操作顺序的不确定性导致错误结果(例如 if(condition) 然后 do_something(),但 condition 可能被其他线程改变)。

如何保证

将非线程安全的代码或数据通过同步原语保护起来,使得并发访问变得有序和可控。

锁:

std::mutex在访问共享资源前lock(),访问完毕后unlock()

std::lock_guard:简单的RAII封装,构造时加锁,析构时解锁。适用于明确的临界区。

std::unique_lock:更灵活的RAII封装,可以延迟加锁、手动解锁,支持条件变量。

原子操作:

std::atomic :计数器、标志位等简单的内置类型(int, bool, pointer等)的场景

使用读写锁 - 优化“读多写少”的场景:

读锁(共享锁):std::shared_lock<std::shared_mutex> lock(mutex);

写锁(独占锁):std::unique_lock<std::shared_mutex> lock(mutex);

介绍多线程编程

多线程编程是一种允许单个程序并发执行多个任务的编程范式。它能够充分利用多核处理器的计算能力,提高程序的响应速度、吞吐量和资源利用率。在现代C++中,多线程支持主要通过 C++11 标准引入的 <thread> 标准库 来实现。

  • 线程是程序执行的最小单元。主函数运行在主线程上,可以通过创建 std::thread 对象来生成新的子线程。

  • 线程对象在构造时即开始执行(与平台相关的分离或连接属性),传入的参数可以是任何可调用对象(函数、Lambda表达式、函数对象、成员函数指针等)。

当多个线程共享数据时,同时的读写操作会导致数据竞争

互斥量(std::mutex):用于保护共享数据。线程在访问数据前锁定(lock()) 互斥量,访问完成后解锁(unlock()),确保同一时间只有一个线程能进入临界区。

条件变量(std::condition_variable):用于让线程在某些条件不满足时等待,并在条件满足时被唤醒,实现线程间的协同。

原子操作(std::atomic):对于简单的计数器或标志位,使用互斥锁开销过大,而使用 std::atomic 对特定类型的操作(如 load, store, fetch_add)保证是原子的、不可中断的。

0

评论区