腾讯 QQ-客户端 一面
1、单例模式的注意事项,有哪些需要注意的
答案:单例模式本身不难,难的是细节。真正要注意的点主要是线程安全、生命周期、析构时机、拷贝控制和全局依赖污染。如果是多线程环境,初始化过程必须保证线程安全;如果单例里持有文件句柄、网络连接、共享内存句柄这类资源,还要考虑进程退出时能不能安全释放。另外单例对象一般都不应该允许拷贝和赋值,否则“全局唯一”这个语义就被破坏了。还有一个很容易被忽略的问题是,单例虽然方便,但会让模块之间形成隐式依赖,测试时也不太好替换,所以不能什么都往单例上套。
如果用 C++11 之后的写法,通常直接用局部静态对象实现就够了,初始化是线程安全的。
代码:
class Singleton {
public:
static Singleton& instance() {
static Singleton obj;
return obj;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
2、简单说一下线程池的原理
答案:线程池的核心思路就是预先创建一批工作线程,任务来了以后不再频繁创建和销毁线程,而是把任务投递到一个共享任务队列里,由空闲线程取任务执行。这样做主要是为了降低线程创建销毁成本,控制并发数量,避免系统在高并发下被大量线程拖垮。
一个完整线程池通常会有这几个部分:任务队列、工作线程集合、同步机制、退出控制。主线程负责提交任务,工作线程阻塞等待队列里的任务,拿到任务就执行;如果线程池关闭,就通知所有工作线程退出。如果面试官继续问深一点,还可以讲到任务窃取、动态扩缩容、拒绝策略、无界队列和有界队列的区别。
代码:
#include <thread>
#include <queue>
#include <vector>
#include <mutex>
#include <condition_variable>
#include <functional>
class SimpleThreadPool {
public:
explicit SimpleThreadPool(size_t n) : stop_(false) {
for (size_t i = 0; i < n; ++i) {
workers_.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this] {
return stop_ || !tasks_.empty();
});
if (stop_ && tasks_.empty()) return;
task = std::move(tasks_.front());
tasks_.pop();
}
task();
}
});
}
}
void submit(std::function<void()> task) {
{
std::lock_guard<std::mutex> lock(mtx_);
tasks_.push(std::move(task));
}
cv_.notify_one();
}
~SimpleThreadPool() {
{
std::lock_guard<std::mutex> lock(mtx_);
stop_ = true;
}
cv_.notify_all();
for (auto& t : workers_) {
if (t.joinable()) t.join();
}
}
private:
std::vector<std::thread> workers_;
std::queue<std::function<void()>> tasks_;
std::mutex mtx_;
std::condition_variable cv_;
bool stop_;
};
3、进程间大数据通信方案,你怎么选择
答案:如果是大数据量、同机房同机器上的多个进程通信,我优先考虑共享内存。原因是共享内存避免了多次用户态和内核态之间的数据拷贝,吞吐量和延迟都比较有优势。如果数据量不大,但更关注编程简单性和边界清晰,可以选 pipe、socketpair、Unix Domain Socket。如果是跨机器,那就不是进程间通信了,一般要走 TCP、RDMA 或者应用层协议栈。
选择时我一般看这几个维度:数据量大小、是否需要跨主机、通信双方关系是否固定、是否需要可靠顺序语义、实现复杂度是否可控。像配置同步、状态通知这种小消息,没必要上共享内存;但如果是本机多进程之间传大块数据,比如图像帧、批量样本、列式数据块,那共享内存会更合适。
4、共享内存的使用流程是什么样子的
答案:共享内存的基本流程就是:创建或打开共享内存对象,设置大小,把它映射到进程地址空间,然后双方通过这块映射区域读写数据,使用完成后解除映射并回收。如果是 System V 共享内存,通常是 shmget、shmat、shmdt、shmctl;如果是 POSIX 共享内存,通常是 shm_open、ftruncate、mmap、munmap、shm_unlink。
流程本身不复杂,真正难的是同步和布局。共享内存里不能只想着“能写进去”,还要考虑头部怎么设计、偏移量怎么维护、生产者消费者怎么同步、异常退出后怎么恢复现场。
代码:
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
int main() {
const char* name = "/demo_shm";
int fd = shm_open(name, O_CREAT | O_RDWR, 0666);
ftruncate(fd, 4096);
void* ptr = mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
std::strcpy((char*)ptr, "hello shared memory");
std::cout << (char*)ptr << std::endl;
munmap(ptr, 4096);
close(fd);
shm_unlink(name);
return 0;
}
5、共享内存有哪些接口
答案:共享内存主要分两类接口。
一类是 System V:shmget 用来创建或获取共享内存段,shmat 把共享内存挂接到进程地址空间,shmdt 解除挂接,shmctl 做控制操作,比如删除、查看状态。
另一类是 POSIX:shm_open 创建或打开共享内存对象,ftruncate 设置大小,mmap 映射到进程空间,munmap 解除映射,shm_unlink 删除对象。
现在工程里很多人更习惯 POSIX 这套,因为接口风格和文件描述符体系更统一,配合 mmap 也更自然。
6、共享内存线程安全吗?怎么保护
答案:共享内存本身只是“共享一块地址空间映射”,它不提供线程安全,也不提供进程安全。多个线程或者多个进程同时读写共享内存,如果没有额外同步,照样会出现竞态、脏读、覆盖和数据结构损坏。
保护方式通常要看场景。如果是多线程共享,最直接就是互斥锁、读写锁、原子变量、条件变量。如果是多进程共享,可以用进程共享互斥锁、信号量、futex,或者基于原子变量实现无锁结构。另外还要注意一个问题:同步原语本身如果也要跨进程共享,必须放在共享内存里,并且初始化时设置成 PTHREAD_PROCESS_SHARED。
代码:
#include <pthread.h>
pthread_mutex_t mtx;
pthread_mutexattr_t attr;
void init_process_shared_mutex() {
pthread_mutexattr_init(&attr);
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(&mtx, &attr);
}
7、线程下面开辟多个协程,等于说一个线程可以有多个协程?
答案:可以。协程本质上是用户态调度单元,不是操作系统内核线程。一个线程里完全可以跑很多个协程,这些协程共享同一个线程的地址空间和内核执行流,只是在用户态主动切换执行上下文。所以“一个线程多个协程”是协程模型里非常常见的
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.
