腾讯 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 共享内存,通常是 shmgetshmatshmdtshmctl;如果是 POSIX 共享内存,通常是 shm_openftruncatemmapmunmapshm_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 Vshmget 用来创建或获取共享内存段,shmat 把共享内存挂接到进程地址空间,shmdt 解除挂接,shmctl 做控制操作,比如删除、查看状态。

另一类是 POSIXshm_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++ 常考面试题总结 文章被收录于专栏

本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务