腾讯二面

这是很久之前的面试了,当时面了两个多小时,很多问题想不起来,后面再补...

项目

  • 日志系统的输出级别?
  • 同步和异步输出什么意思?
  • 日志队列怎么设计的?
  • 有没有容量限制?
  • 怎么刷盘的?
  • 怎么应对大量日志写入的情况?

C++

Q:make_shared和构造函数传裸指针有什么区别?

A:(这个问题已经在一面说过了...)shared_ptr有两个指针成员:1) 资源指针p,2) 控制器指针c;控制器中由几部分组成,资源、分配器、删除器、强弱引用计数;如果使用make_shared,那么是直接new 整个控制器,并通过placement new对其中的资源指针原地构造,然后对指针p赋值;如果使用构造函数裸指针或者reset,那么肯定也是需要new 控制器的,但是少了原地构造的过程,直接对控制器中的资源指针进行赋值。(我顺带提了一嘴,很多人总在比较这两个性能,我觉得并不是一个层面上的比较)

Q:那你是怎么想的?

A:如果只比单个函数的话,make_shared需要申请更大空间,并且要原地构造,但是构造函数不需要这些,所以make_shared要稍微慢一些;但是如果加上new裸指针的消耗,那么make_shared只new一次的操作应该更加高效一些,这其中可能涉及到内存碎片等等;

Q:你平时更习惯使用哪一种呢?

A:要是我的话,可能更加倾向于make_shared;这是因为,如果传入裸指针,但又显式的delete,那么当shared_ptr退出作用域后,又会进行一次delete,造成程序异常。其实这归根到底是因为,shared_ptr没有感知资源是否释放的能力;这一点很像返回shared_ptr的this指针,C++中的奇异模板重现模式,可以用来解决这个问题,具体的类叫做enable_shared_from_this;

Q:介绍一下C++ memory_order(考虑到很多人不了解,这里展开叙述)?

在C++11之后,引入了并发支持库,说到多线程那就不可避免的会遇到数据竞争和线程同步等问题,C++提出了互斥锁mutex和原子变量atomic等,对于原子变量的操作,有一个很重要的概念叫做内存顺序;这其中会涉及到多处理器的指令重排(提高性能)等,使得线程的执行顺序在不同的处理器看来是不同的。可能很多Cpp-er在使用atomic的时候并没有考虑过这么多,甚至感觉这是没有必要的?这是为什么呢?这时因为atomic模板类的默认内存顺序是最高级别的,也就是说,它的内存顺序保证线程的执行顺序是全局一致的,类似于mysql中的最高隔离级别串行化,虽然可以解决问题,但是效率一般。

  • 原子变量的操作:store,load,read-modify-write (例如fetch_add,exchange等操作)
  • 内存顺序:
enum memory_order {
    memory_order_relaxed,	// 不对执行顺序做任何保证
    memory_order_consume,	// 后续有关本原子类型的操作,必须在本原子操作完成后执行
    memory_order_acquire,	// 后续的读操作必须在本原子操作完成后执行
    memory_order_release,	// 所有之前的写操作必须写完后才能执行本原子操作
    memory_order_acq_rel,	// 同时包含 acquire, release
    memory_order_seq_cst,	// 全局一致性, 按照代码顺序执行
};

atomic默认memory_order_seq_cst原子顺序:

  • 修改顺序:Happens-before:单线程的情况: sequenced-before;多线程的情况: synchronizes-with 和 inter-thread happens-before

  • 顺序一致内存顺序/memory_order_seq_cst:

在这个顺序模型下,所有线程看到的实际操作都有一致的顺序,即禁止指令重排,按照代码顺序执行;这种比较好理解,实现sequencial consistent 模型有一定的开销,现代 CPU 通常有多核, 每个核心还有自己的缓存,为了做到全局顺序一致,每次写入操作必须同步给其他核心;为了减少性能消耗,如果不需要全局一致性,可以考虑使用更加宽松的顺序模型;

  • 宽松内存顺序/memory_order_relaxed:这是另一个极端,只能保证读写的原子性,但是彼此的顺序不做要求,例如:
std::atomic<bool> x{false}, y{false};

void thread1() {
    x.store(true, std::memory_order_relaxed); // (1)
    y.store(true, std::memory_order_relaxed); // (2)
}

void thread2() {
    while (!y.load(std::memory_order_relaxed)); // (3)
    assert(x.load()); // (4)
}

由于使用宽松内存模型,所以(1)和(2)处的顺序不做要求,那么如果(2)先执行,再执行(3),再执行(4),就会断言失败;反之断言成功。Relaxed 顺序模型的开销很小,可以用在一些不需要线程同步的场景, 但是使用时要小心。

  • Release-acquire内存顺序:这个顺序模型相比前两者就比较中肯,主要借助这三种内存顺序:

memory_order_acquire:后续的读操作必须在本原子操作完成后执行

memory_order_release:所有之前的写操作必须写完后才能执行本原子操作

memory_order_acq_rel:同时为上面两者

借助Release-acquire可以实现线程同步的要求:

std::atomic<bool> x{false}, y{false};

void thread1() {
    x.store(true, std::memory_order_relaxed); // (1)
    y.store(true, std::memory_order_release); // (2)
}

void thread2() {
    while (!y.load(std::memory_order_acquire)); // (3)
    assert(x.load(std::memory_order_relaxed)); // (4)
}

这和上面例子中的代码基本一致,区别在于变量y读写的内存顺序;但是(1)肯定happen before(2),那么(4)处肯定可以读取到(1)的结果,因此断言成功。值得说一句,shared_ptr在增加和减少引用计数的时候使用的就是acq_rel或者acq等顺序。

  • Release-consume内存顺序:memory_order_consume简单来说,就是 所有后续的有关本数据的操作,必须在本条原子操作完成之后执行;举个简单的例子:
std::atomic<std::string*> ptr;
int data;

void thread1() {
    std::string* p  = new std::string("Hello"); // (1)
    data = 42; // (2)
    ptr.store(p, std::memory_order_release); // (3)
}

void thread2() {
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_consume))); // (4)
    assert(*p2 == "Hello"); // (5)
    assert(data == 42); // (6)
}

release-consume组成的内存模型,会使得(5)的断言肯定成功,但是由于data和该原子变量没有关系,所以对他的读写顺序不做要求,因此(6)处的断言可能会失败;不难看出,consume对指令重排的限制要更加宽松一些,性能会更好一些 (相比于acq)。ps:还值得一提的是,在cpp官方文档中,指出关于consume的描述还在修改,不建议使用consume。

参考:C++11 - atomic类型和内存模型谈谈 C++ 中的内存顺序 (Memory Order)

操作系统

Q:线程池的数目设置?

A:提供了api可以由用户设置;默认取本机的逻辑处理器数;thread::hardware_concurrency

Q:无限制创建线程会有什么问题?

A:线程也会占用资源,例如内存等,无限制的申请线程,肯定会造成内存溢出或者系统崩溃;

Q:线程切换到底切换了什么?

A:和进程相比,线程仅仅保存少量的资源,所以,当发生cpu调度的时候,操作系统会保存当前线程的寄存器的值,例如程序计数器,栈顶位置以及CPU中间状态的值等;然后内核会执行线程切换函数,从就绪队列中选择一个线程来执行,并恢复线程对应的CPU上下文;如果是两个进程间的线程发生切换,那么会涉及到进程的调度和切换,开销更大。

Q:场景题:现在有一个线程在执行一个扫描文本的任务,该文本中有图片链接,所以当线程扫描到这里时,需要开一个异步任务去下载图片,但是这个异步任务被丢到当前线程执行,问这个场景能否运行?为什么?

A:(其实我没理解这个问题,因为我这不知道这种场景具体怎么能实现?)我只能瞎扯说,我觉得应该不能正常执行,因为当前线程还在运行,而你又想给它设置一个新的任务,从逻辑上行不通;一般可以再开一个线程去执行这个异步任务,或者直接同步阻塞下载图片。(后来我想到,这会不会是在问抢占式调度呢?但是感觉也不像呀?如果有佬理解这个问题,可以评论一下)

Q:有没有了解过其他定时器的实现方式?

A:其实不同的实现方式都是为了要快速找出过期任务,那么可以使用的方案就很多了:

  1. 维护有序数组,二分查找过期任务;
  2. 类似1,可以借助于跳表;
  3. 使用红黑树维护;
  4. 使用最小堆维护;
  5. 使用时间轮算法:对于时分秒三级时间,每一级都使用一个时间轮,并维护一个游标cursor:

Q:互斥锁和自旋锁?

A:互斥锁:加锁失败会阻塞,让出cpu自旋锁:加锁失败并不会阻塞,而是继续去获取锁,逻辑上很像while循环,一直自旋,直到锁可用;

Q:自旋锁的实现?

A:自旋锁实现的方式也有不少,我了解的可以通过TAS(TestAndSet) 和 CAS(CompareAndSwap)指令实现;TAS本质就是set并返回old值,如果old为0才可以结束while;CAS和TAS差不太多,不过要稍微多一些寄存器的使用,包含的信息更多,可以使用32/64bits的数据类型;只有当reg的值和old一致才可以退出while;

Q:自旋锁的优点以及使用场景?

A:从本质来讲,自旋锁不会阻塞让出CPU,不存在线程调度已经上下文的切换了,因此如果可以确保锁可以很快被释放,即锁的粒度很小,执行很快,竞争不激烈的场景可以使用自旋锁,效率很高;

Q:自旋锁的缺点?以及改进方法?

A:如果长时间上锁,那么由于不会主动让出处理机,因此会做很多无用功,带来性能消耗;改进方法,我暂时可以想到的是,可以设置一个时间阈值,自旋时间超过阈值,就主动退出;

数据库

Q:什么是索引?

A:可以理解为目录,用于快速查找...(具体不多bb)

Q:了解MySQL使用的引擎吗?

A:MyISAM,InnoDB,Memory等,对InnoDB有一定了解

Q:为什么采用B+树呢?

A:不多bb...

Q:有没有B树的引擎呢?或者说B树适用于什么样的场景呢?

A:(我还真不知道有没有B树的引擎) 我当时说,应该是有的,但是这个确实了解不多;但是应用的场景,B+树的数据节点存在最底层,所以只有访问到最下面的节点才可以获取数据,相比而言,B树就没有这种限制,因此B树可能在单点查询频繁的场景比较用;

计算机网络

Q:Https的流程?

A:TCP+TLS,不多bb...

Q:客户端如何确保CA机构的合法性?

A:(这个我答得一般...)我说,一般在浏览器或者操作系统内部都是有内置的一些CA机构的公钥,然后使用该公钥对证书的数字签名进行解密,然后可能会自己再根据证书中的服务器信息计算一个hash值,两者比较,确认证书的合法性;确实,我们在访问网页的时候,肯定或多或少都会遇到一种情况:该网站安全证书有问题怎么怎么的,这种就是不合法的情况。

口述算法

  • (典型)rand5生成rand7
  • 数组寻找重复数

代码环节

  • 五分钟,写一个模板的LRU算法,支持任意类型数据的存取
#我的实习求职记录##牛客解忧铺##24届软开秋招面试经验大赏#
2024秋招分享 文章被收录于专栏

2024秋招分享

全部评论
五分钟一个模板LRU这么猛嘛
点赞 回复 分享
发布于 2023-09-22 01:18 四川
问下哪个部门, 还有项目只有一个webserver吗
点赞 回复 分享
发布于 2023-09-22 11:14 山东
问gpt的
点赞 回复 分享
发布于 2023-09-22 22:07 英国
学姐加油!
点赞 回复 分享
发布于 2023-09-23 16:30 辽宁

相关推荐

11-24 11:23
门头沟学院 C++
点赞 评论 收藏
分享
10-25 23:12
门头沟学院 Java
点赞 评论 收藏
分享
点赞 评论 收藏
分享
评论
9
43
分享
牛客网
牛客企业服务