面灵AI→

虾皮研发一面 国庆也能收到感谢信了

轮次
一面
时间
2026-09
来源
牛客网

《面试题目》

  1. Agent 会不会依赖 reference?会不会覆盖不到边缘场景?
  2. 怎么评测召回的准确率?测试集数量是多少?
  3. 平台的日活量大概是多少?
  4. 新的模型出来后怎么适配之前的 Agent 项目?这对 Agent 设计有什么影响?
  5. 在浏览器里输入一个网址,之后发生了什么?
  6. TCP 的拥塞控制是怎样的?在当前网络带宽很大的情况下它有什么局限性,需要怎么调整?
  7. 进程在一个地址写了数据,另一个进程在没有共享内存的情况下能读到这个数据吗?应该怎么让另一个进程读到?
  8. 两个进程希望通过共享内存通信,传输 1KB 的数据应该怎么做?
  9. 自旋锁和互斥锁有什么区别?为什么不都用自旋锁?
  10. 有一个接口服务 QPS 比较高,每秒大概 100 个请求进来,需要统计这个接口每秒的总调用次数。架构上是 100 个工作线程处理请求,另外有一个单独的统计线程负责汇总所有线程的计数,你会怎么设计这个计数结构?如果这个服务是分布式集群部署的,计数数据会被频繁修改,同时需要保证分布式环境下的数据一致性,你会怎么调整方案?
  11. 现在有 100 个工作线程,每秒总共产生一万次请求,其中绝大多数都是调用 config 的 get 方法读取配置,大概每秒一万次读操作,而配置的 update 更新频率很低,每十分钟才执行一次。针对这种典型的读多写少场景,你会怎么设计这个配置模块?
  12. 读写锁的底层实现原理是什么?传统读写锁在写锁释放后需要向所有等待的读线程广播唤醒信号,这个广播开销在高并发下比较明显,有没有比传统读写锁更好的优化方法?
  13. 如果是在多核 CPU 架构下,跨 CPU 核心的缓存同步和锁广播开销会更突出,针对这种多核 CPU 之间的广播问题,你会怎么设计优化?
  14. 更泛化地讲,针对多线程运行在多核 CPU 的场景,你做并发数据结构设计的核心思路是什么?
  15. 如果配置的数据大小不确定、可能会比较大,你刚才提到的每个线程维护一份配置副本的方案内存开销会很大,这个问题要怎么解决?
  16. DAG 怎么做拓扑排序,要按顺序列出来?DAG 只有一个入度为 0 和出度为 0 的节点,怎么找图里从入口到出口有多少条不同路径?

《参考解析》

  1. TCP 拥塞控制在大带宽下的局限:慢启动从很小的拥塞窗口开始指数增长,带宽时延积(带宽 × RTT)越大,爬到能跑满带宽的窗口所需时间越久,短连接几乎全程都在慢启动里,跑不出带宽。AIMD 收到丢包就把窗口减半,在无线或有随机丢包的链路上会把非拥塞丢包当成拥塞,窗口长期上不去。调整方向有几层:把初始窗口调大(RFC 6928 的 10 个 MSS)、换成 CUBIC 或 BBR 这类不只把丢包当信号的算法、开启窗口缩放与 SACK、调大 socket 收发缓冲区,以及在上层减少往返次数(连接复用、多路复用、批量请求)。回答时把「为什么大带宽反而吃亏」讲清楚——瓶颈从带宽变成了 RTT 和丢包判定。

  2. 自旋锁与互斥锁的取舍:互斥锁拿不到就睡眠让出 CPU,适合临界区较长、竞争激烈的场景;自旋锁拿不到就在原地忙等,不进内核调度,适合临界区极短且持锁时间可预期的场景,省掉两次上下文切换。不能全用自旋的原因是:单核或线程数多于核数时,自旋者占着 CPU 不让持锁者跑,纯属空烧;临界区一长,等待时间直接变成 CPU 空转;另外自旋期间如果持锁线程被抢占或发生缺页,等待会被无限放大。工程上多用「先自旋几次再挂起」的自适应锁,内核里的 mutex 就有 optimistic spinning。

  3. 100 个线程的高频计数怎么设计:核心是消除共享写热点。单机方案是让每个线程在自己的缓存行上累加(用 padding 或 @Contended 避免伪共享),读的时候再汇总;或者直接用 LongAdder,它内部就是 Cell 数组分散热点、只在求和时聚合,比 AtomicLong 的单点 CAS 自旋扩展性好得多。改成分布式集群后,别让 100 个节点对同一个 Redis key 做 INCR——热点 key 加跨网络往返会成为新瓶颈;做法是节点本地按时间窗聚合、定时批量上报,汇总侧把数据打散到多个分片 key 或按时间分桶再求和。对统计类指标可以接受最终一致加定期对账,比强一致便宜得多,这个取舍要主动说出来。

  4. 读多写少的配置模块:思路是让读路径完全不加锁。用一份不可变配置对象加原子引用,读线程只读引用、拿到的必然是一个完整版本,写线程构造新对象后一次性替换引用即可,读侧无阻塞无计数器争用;再挂一个版本号或做 copy-on-write,替换和回滚都可控。传统读写锁的短板正好在这里——读锁也要改共享的计数器(同样有缓存行争用),写锁释放还要广播唤醒所有等待的读线程,读越多这两处越贵。所以高并发读场景常用 StampedLock 的乐观读或纯不可变加引用替换。

  5. 多核下的缓存同步与并发结构设计:真正的开销来自缓存一致性协议——一个核写过的缓存行在其他核上都要失效,谁读谁要重新拉取,锁释放还得广播唤醒。设计思路按优先级排:能不共享就不共享,数据分片到线程本地(thread-local、per-CPU 变量)只在必要时聚合;必须共享时按缓存行隔离,避免伪共享;把共享写改成批量或追加,降低写频率;用乐观读(版本号加校验)替代加锁读,把同步成本压到一次校验。数据量大到不能每线程一份副本时,就只共享索引或句柄,实际内容按需分页加载——这正是第 15 问的标准答法。

  6. 共享内存与进程间通信:进程各有独立虚拟地址空间,A 在自己地址写的数据 B 读不到,即使物理页复用也不行。要让 B 读到,必须把同一段物理内存映射进两个进程:System V 的 shmget/shmat、POSIX 的 shm_open 加 mmap,或者匿名 mmap(MAP_SHARED) 配合 fork。只传 1KB 这种小数据,共享内存的映射与同步开销未必划算,管道、Unix domain socket 或消息队列更简单;真要用共享内存,注意它本身不提供同步,必须配信号量或 futex,还要处理进程崩溃后的残留清理与生命周期管理。

  7. DAG 拓扑排序与路径计数:拓扑排序用 Kahn 算法——统计各点入度,入度为 0 的先入队,逐个出队并把指向的节点入度减一,减到 0 就入队,出队顺序即拓扑序;也可以用 DFS 后序取反。路径条数在拓扑序上做动态规划:dp[v] 等于所有前驱 dp[u] 之和,入口节点置 1,出口节点的值就是答案。这个递推只有在拓扑序上才成立,所以必须先排序再计数;入度、出度都为 0 的节点各只有一个,说明图是单入口单出口,不需要额外处理多源多汇。