死锁产生的条件,如何避免
死锁需要同时满足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_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)保证是原子的、不可中断的。
评论区