文章目录
//评测题目: 无
一、项目
1、集群有什么评测指标?老师对同学有什么要求指标?
功耗,散热,
响应时间
系统处理能力
吞吐量
并发用户数
错误率
2、节点挂了能感知吗?有什么管控策略消除节点隐患?会等待吗?挂了会怎么样?怎么解决?
主节点轮询探活,维护两个list,一个能工作,一个暂时挂掉的
3、探活,怎么探活?增加一个树莓派?
4、网络连不上,怎么搞定的?
一个树莓派可以获取自己的ip,可以ping内网,但不能ping外网,切换其他树莓派主机使用同样的ip地址可以连通外网,说明路由器没问题,上网查得知可能是路由器无法识别第一个树莓派的网卡地址,修改网卡地址就好了。
5、brpc的架构组成?
lianjei
客户端:channel,
ns,brpc::NamingService
LB:load balance
socket
brpc是一个基于protobuf接口的RPC框架
6、面向对象常见的设计模式?
一些原则?
工厂方法模式(接口)
抽象工厂模式
单例设计模式(多线程)
建造者模式
原型模式
适配器模式,装饰器模式
代理模式,桥接模式
模版模式,解释器模式,命令模式,中介者模式,备忘录模式
7、6大原则
开闭原则 :支持扩展增量不支持修改
1、单一职责原则:一个类只完成单一职责,
2、里氏替换原则,能使用基类的地方就能用子类
3、依赖倒转原则:面向接口编程,
4、接口隔离原则:每个接口不存在子类用不到却必须实现的方法,
5、知道最少原则:一个类对自己依赖的类知道的越少越好,依赖类的封装,减少依赖类改变而造成的影响
6、合成复用原则:少用继承
8、一个大功能模块划分的依据?
划分一个大功能模块的依据可以基于多个因素,这些因素有助于确保模块划分后的模块具有高内聚(模块内部功能高度相关)和低耦合(模块之间的依赖关系尽可能少)的特点。以下是一些常见的依据:
单一责任原则(Single Responsibility Principle):
每个模块应该专注于完成单一的功能或责任。如果一个大功能模块包含多个不相关的子功能,可以考虑将其划分为多个模块,每个模块专注于一个子功能。
功能独立性:
每个模块应该在逻辑上是相对独立的,即模块内部的功能和操作不依赖于外部模块过多的实现细节或状态。
通信频率:
如果某些功能需要频繁通信或共享数据,考虑将这些功能放置在同一个模块中,以减少模块间的通信成本和复杂度。
代码复用:
如果某些功能在不同的上下文中可以被复用,可以考虑将其划分为独立的可复用模块,以提高代码的重用性和可维护性。
抽象和实现的分离:
将高层次的抽象和底层的实现分离为不同的模块。抽象模块负责定义接口和逻辑,而实现模块负责具体的算法或操作。
业务边界:
根据业务逻辑和边界确定模块划分的依据。将功能相似或在业务逻辑上关联紧密的部分划分到同一个模块中,以支持更清晰的业务流程和逻辑。
团队组织和协作:
考虑团队的组织结构和工作流程,将不同团队或开发者负责的功能模块划分清晰,以便于分工合作和项目管理。
技术依赖:
考虑不同技术栈或技术依赖关系,将技术相关性强的功能或模块放置在同一个单元中,以便于技术选型和开发实施。
在实际应用中,通常需要综合考虑上述多个因素,以确定最合适的大功能模块划分依据。这样能够确保划分后的模块结构清晰,便于理解、维护和扩展。
8.1、常见的线程实现同步的方法包括:
互斥锁(Mutex):
使用互斥锁可以确保在同一时刻只有一个线程访问共享资源。线程在访问共享资源之前会尝试获取互斥锁,如果锁已被其他线程占用,则会被阻塞直到锁被释放。
信号量(Semaphore):
信号量可以用来控制同时访问的线程数目,不仅可以实现互斥访问,还可以控制最大并发数。信号量有两种类型:计数信号量和二进制信号量(互斥量)。
条件变量(Condition Variable):
条件变量允许线程在满足特定条件之前等待。通常与互斥锁一起使用,用来实现线程的等待和通知机制。等待线程在满足条件前会被阻塞,当条件满足时,其他线程可以通过通知来唤醒等待线程。
读写锁(Read-Write Lock):
读写锁允许多个线程同时读取共享资源,但是在写操作时需要互斥。适合读操作频繁、写操作较少的场景,可以提高并发效率。
原子操作:
原子操作是不可中断的操作,确保在并发环境中对共享变量的操作是原子的,即不会被中断。通常使用原子类型或者专门的原子操作函数来实现。
这些方法通常可以根据具体的并发需求和性能要求来选择使用,以确保线程安全和避免竞态条件的发生。
8.2 常见的线程实现异步的方法包括:
Future 和 Promise:
Future 和 Promise 是 C++11 引入的一种异步编程模型,Future 用于获取异步操作的结果,而 Promise 用于设置异步操作的结果。通过 Future 可以在一个线程中启动一个异步任务,并在需要时获取其结果。
回调函数(Callback):
回调函数是一种常见的异步编程模式,通过注册一个回调函数,在异步任务完成后会调用该函数来处理结果。这种模式在事件驱动的编程中经常使用,例如 GUI 编程和网络编程。
事件循环(Event Loop):
事件循环是一种通过循环检查事件队列的方式实现异步的方法。在事件驱动的编程模型中,主线程或专用线程会在一个循环中等待事件的到来,并且在事件发生时处理相应的事件处理函数。
线程池(Thread Pool):
线程池是预先创建好一定数量的线程,并且维护一个任务队列。当有任务到来时,可以将任务提交给线程池执行,而不是为每个任务创建新的线程。这样可以减少线程创建和销毁的开销,提高效率。
异步任务库:
许多编程语言和框架提供了专门用于管理和调度异步任务的库或模块,例如 C++ 的 Boost.Asio、Python 的 asyncio 等。这些库通常提供了高级的异步操作接口,能够简化异步编程的复杂性。
协程(Coroutine):
协程是一种轻量级的线程,可以在单个线程内实现多个并发执行的函数。通过协程可以实现异步操作而不需要创建多个线程,提高资源利用率和程序性能。
这些方法各有特点,适合不同的应用场景和编程需求。选择合适的异步编程方法可以有效提高程序的响应性和并发能力。
8.3 常用的线程通信方法包括:
共享内存:
多个线程通过访问共享的内存区域来进行通信。通常需要使用互斥锁或者其他同步机制来保护共享内存的访问,以防止竞态条件的发生。
消息队列:
消息队列是一种异步通信的方式,用于在多个线程之间传递消息。每个线程都有一个消息队列,可以往队列中发送消息,同时从队列中接收消息。消息队列可以基于内存或者基于操作系统的实现。
管道(Pipe):
管道是一种半双工的通信机制,允许两个线程之间进行单向的通信。一个线程向管道写入数据,另一个线程从管道读取数据。通常用于父子进程或者通过线程间的通信。
信号量(Semaphore):
信号量不仅可以用来控制同一时间内多个线程的访问数目,还可以用来进行线程间的同步和通信。通过信号量的计数机制,可以实现线程的等待和唤醒操作。
条件变量(Condition Variable):
条件变量通常与互斥锁结合使用,用于线程的等待和通知。一个线程可以在满足特定条件之前等待,而其他线程可以通过条件变量来通知等待线程条件已经满足。
消息传递:
消息传递是一种更高层次的通信方式,通过发送和接收消息来进行线程间的通信。可以基于队列、信号量等低层次的通信机制实现。
事件(Event):
事件是一种同步和通信机制,允许一个线程通知其他线程发生了某个事件。通常使用事件来实现线程的等待和唤醒,或者进行线程间的状态同步。
这些线程通信方法各有特点,适合不同的应用场景和需求。在选择和设计线程通信方法时,需要考虑线程安全、效率和编程复杂度等因素。
8.4 常用的进程通信方法包括:
管道(Pipe):
管道是一种基本的进程间通信(IPC)方式,分为无名管道和命名管道。无名管道通常用于父子进程间通信,而命名管道则可以用于任意两个进程间通信。
消息队列:
消息队列是一种通过消息传递进行通信的IPC方式。进程可以往消息队列发送消息,也可以从消息队列接收消息。消息队列通常具有先进先出的特性,并且支持多进程之间的消息传递。
共享内存:
共享内存是一种高效的IPC方式,允许多个进程访问同一块内存区域。共享内存通常需要配合信号量等同步机制来确保多个进程之间的数据一致性和安全性。
信号量(Semaphore):
信号量可以用来控制多个进程对共享资源的访问,同时也可以用作进程间的同步和通信。进程可以通过信号量来等待和通知事件的发生。
套接字(Socket):
套接字是一种通用的进程间通信方式,不仅限于同一台计算机内的进程通信,还可以用于网络上不同计算机之间的通信。套接字通常用于实现客户端-服务器模式的通信。
共享文件:
进程可以通过共享文件来进行通信。多个进程可以对同一个文件进行读写操作,从而实现数据交换和通信。文件通常需要配合文件锁等机制来确保多进程之间的数据一致性。
信号(Signal):
信号是一种异步通信机制,用于通知进程发生了某个事件。例如,进程可以向另一个进程发送信号来请求其执行某个特定的操作,如终止等。
这些进程间通信方法各有特点,选择适当的方法取决于通信的需求、进程之间的关系以及安全性等因素。
9、怎么提高吞吐量?
大部分程序员都是工作在应用层,针对应用级别代码能提高吞吐量的建议:
加大应用的进程数,增加并发数,特别在进程数是瓶颈的情况下
优化线程调用,尽量池化。
应用的代码异步化,特别是异步非阻塞式编程对于提高吞吐量效果特别明显
充分利用多核cpu优势,实现并行编程。
减少每个调用的响应时间,缩短调用链。例如通过加索引的方式来减少访问一次数据库的时间
https://segmentfault.com/a/1190000020672248
10、集群哪里用到异步?
-
使用非递归的深度优先搜索算法,遍历一个树状的数据结构,并给出一个实际业务应用场景的例子。
图片搜索,文件夹! -
某地停车场大楼有5层,每层1000个停车位,有分别在大楼四周设有4个出入口。请写一段代码来管理停车位,要求:尽可能确保停车用户的体验。
公共资源并行读写,多线程读写。
解释代码功能:
ParkingLot 类:表示停车场,包括多层、每层停车位数、每层可用停车位数量以及每层的互斥锁。
park() 方法:尝试在一个可用的楼层停车,并更新可用停车位数量。使用互斥锁保护对可用停车位的修改。
leave() 方法:模拟车辆离开,释放停车位并更新可用停车位数量。同样使用互斥锁保护修改操作。
checkAvailability() 方法:检查整个停车场的总可用停车位数量,使用互斥锁保护读取操作。
displayStatus() 方法:显示每层的停车位状态,同样使用互斥锁保护读取操作。
在 main() 函数中,模拟多个线程同时进行停车和离开操作,通过线程安全的方法管理共享资源,确保停车用户在并行操作下的正确体验。
#include <iostream>
#include <vector>
#include <mutex>
#include <thread>
using namespace std;
class ParkingLot {
private:
int num_floors;
int spots_per_floor;
vector<int> available_spots; // 1D vector to track available spots per floor
vector<mutex> floor_mutexes; // Mutexes for each floor to ensure thread safety
public:
ParkingLot(int floors = 5, int spots_per_floor = 1000) {
num_floors = floors;
this->spots_per_floor = spots_per_floor;
available_spots.resize(num_floors, spots_per_floor); // Initialize all spots available per floor
floor_mutexes.resize(num_floors); // Initialize mutexes for each floor
}
bool park() {
// Simulate some processing time before attempting to park
this_thread::sleep_for(chrono::milliseconds(100));
for (int floor = 0; floor < num_floors; ++floor) {
lock_guard<mutex> lock(floor_mutexes[floor]); // Lock this floor
if (available_spots[floor] > 0) {
available_spots[floor]--;
cout << "Vehicle parked at Floor " << floor + 1 << endl;
return true;
}
}
cout << "Sorry, parking lot is full." << endl;
return false;
}
void leave(int floor) {
// Simulate some processing time before leaving
this_thread::sleep_for(chrono::milliseconds(50));
if (floor <= num_floors) {
lock_guard<mutex> lock(floor_mutexes[floor - 1]); // Lock this floor
available_spots[floor - 1]++;
cout << "Vehicle left from Floor " << floor << endl;
} else {
cout << "Invalid floor number." << endl;
}
}
void checkAvailability() {
int total_available = 0;
for (int floor = 0; floor < num_floors; ++floor) {
lock_guard<mutex> lock(floor_mutexes[floor]); // Lock this floor
total_available += available_spots[floor];
}
cout << "Total available spots: " << total_available << endl;
}
void displayStatus() {
for (int floor = 0; floor < num_floors; ++floor) {
lock_guard<mutex> lock(floor_mutexes[floor]); // Lock this floor
cout << "Floor " << floor + 1 << ": " << available_spots[floor] << " spots available" << endl;
}
}
};
void parkVehicles(ParkingLot& parkingLot, int num_vehicles) {
for (int i = 0; i < num_vehicles; ++i) {
parkingLot.park();
}
}
void vehiclesLeaving(ParkingLot& parkingLot, int num_vehicles) {
for (int i = 0; i < num_vehicles; ++i) {
int floor = rand() % parkingLot.num_floors + 1; // Random floor
parkingLot.leave(floor);
}
}
int main() {
ParkingLot parkingLot;
// Simulate multiple vehicles trying to park and leave concurrently
thread t1(parkVehicles, ref(parkingLot), 5);
thread t2(vehiclesLeaving, ref(parkingLot), 3);
thread t3(parkVehicles, ref(parkingLot), 2);
t1.join();
t2.join();
t3.join();
// Check availability and display status after threads complete
parkingLot.checkAvailability();
parkingLot.displayStatus();
return 0;
}
2、222
1、extern关键字
第一个,当它与"C"一起连用时,如: extern “C” void fun(int a, int b);则告诉编译器在编译fun这个函数名时按着C的规则去翻译相应的函数名而不是C++的,C++的规则在翻译这个函数名时会把fun这个名字变得面目全非。因为C++支持函数的重载
第2个:此文件外的文件中声明。源代码有编译、链接。
编译时候不报错,链接时候按照指定的路径找到相应的文件,链接成可执行文件。.c 文件.i .s .o .exe
2、innoDB数据结构
16k的页大小
3、linux中fork的过程
读时共享,写时复制,父子进程有相同的地方,有不同的地方,pcb不一样,不一样的栈空间,
4、进程间通信方式
管道,匿名管道,消息队列,本地套接字,共享内存,信号量
5、共享内存通信方式的实现方式?
共享内存的使用实现原理(必考必问,然后共享内存段被映射进进程空间之后,存在于进程空间的什么位置?共享内存段最大限制是多少?)
共享内存段紧靠在栈之下,最大限制为32M
6、操作系统在c++多态中的动作?
7、TCP TIME_WAIT状态,2MSL,计算RTO方式,计算发送一个数据报到接受ack的时间。
2、线程池
#define LL_ADD(item,list) do{ \
item->next=list; \
if(list!=NULL) list->prev=item; \
list=item; \
} while(0)
#define LL_REMOVE(item,list) do {
if(item->prev !=NULL) item->prev->next=item->next; \
if(item->next !=NULL) item->next->prev=item->prev; \
if(item==list) list=item->next; \
item->prev=item->next=NULL; \
} while(0)
struct NJOB{
void (*job_func)(void * arg);
void *arg;
struct NJOB * next;
struct NJOB * prev;
};
struct NWORKER{
pthread_t threadid;
int terminate;
struct NMANAGER * pool;
struct NWORKER * next;
struct NWORKER * prev;
};
struct NMANAGER{
struct NJOB * jobs; // n -- > job
struct NWORKER * workers;
pthread_mutex_t mtx; // 锁
pthread_cond_t cond; // 条件变量
};
// sdk api
// 打饭阿姨,打完一个 下一个
void * threadcallback(void *arg){ // 线程的入口函数
struct NWORKER * worker=(struct NWORKER *)arg;
while(1){
// 1、if (task==null) wait
// 2、get task
// remove task;
pthread_mutex_lock(worker->pool->mtx);
while(worker->pool->jobs==NULL){
pthread_cond_wait(&worker->pool->cond,&worker->pool->mtx);// 加锁
}
struct NJOB * job = worker->pool->jobs;
if(job!=NULL){
LL_REMOVE(job,worker->pool->jobs);
}
pthread_mutex_unlock(worker->pool->mtx);
// 3、task->func();
job->job_func(job);
}
}
// create(struct NWORKER * pool,int numworker){ }// 初始化,一个线程池,有多少个工作的线程
// destory(struct NWORKER * pool){ }
//push_task(struct NWORKER * pool,struct NJOB * job);
typedef struct NMANAGER NTHREADPOOL;
int nThreadPoolCreate(NTHREADPOOL *pool,int nWorker){
// check para
if(pool==NULL) return -1;
memset(pool,0,sizeof(NTHREADPOOL));
if(nWorker < 1) nWorker=1;
// pool --> initialize
pthread_mutex_t blank_mutex=PTHREAD_MUTEX_INITIALIZER;
memcpy(&pool->mtx,&blank_mutex,sizeof(pthread_mutex_t));
pthread_cond_t blank_cond=PTHREAD_COND_INITIALIZER;
memcpy(&pool->cond,&blank_cond,sizeof(pthread_cond_t);
int i=0;
for(i=0;i<nWorker; i++){ // nWorker * worker
struct NWORKER * worker=(struct NWORKER*)malloc(sizeof(struct NWORKER));
if(worker==NULL){
perror("malloc failed!");
return -2;
}
memset(worker,0,sizeof(struct NWORKER));
worker->pool=pool; // 每一个线程都有一个指向线程池管理组件的指针
int ret=pthread_create(&worker->threadid,NULL,threadcallback,worker ); // 返回的线程id,线程属性
if(ret){
perror("pthread_create");
free(worker);
return -3;
}
LL_ADD(worker,pool->workers); // 把创建的线程添加到线程池
}
return 0;
}
2、问的项目:
1、国企问的项目:
自我介绍:
-
1、我叫。。。
xxxx大学计算机专业研三同学, -
2、本科专业也是计算机专业。本科阶段在acm呆过一段时间。
-
3、研究生阶段:从零搭建了一个50个树莓派板子的集群,做的工作是升级树莓派的编译工具链,升级pi的操作系统。为了更充分的利用最新pi版本的硬件特性()。
- 在系统上做的稀疏矩阵迭代算法的加速研究。
- 内存布局:按行存—>内存中不连续的排列布局----->跳跃的存储器访问(因为两层for循环i从0 到n-1行,内层循环从0到 n - 1,)—>
- 访存顺序
- 多线程的并行化(每个块都会有加载和存储过程。搞两个线程。解决数据依赖,传统着色法,按)
-
4、还有一个在dd地图架构部门做过一段时间的后端开发实习生。写的 c++和go,
主要业务是:
集群:
怎么提升的是这个倍数?应该是整数倍啊。40倍,多大的网卡?千兆,网络延迟应该不是啊。分析啥原因?
2、NFS,能共享内存吗?mnt挂载一下就能共享内存了?
3、蚂蚁:备用主节点和主节点之间有状态记录吗?
没有,我们主要是做算法层面的加速,挂了重新算也花费不了太多。
实习:
4、父线程怎么知道子线程结束,不用条件变量。通常怎么做?
从目标,任务,你做了啥?讲一下?
5、百度
1、集群的任务调度,多线程怎么安排?高性能计算这块了解不深入。
只有一个配置文件,配置每个树莓派开多少个进程,静态的读取配置。
任务调度这一块涉及的比较少。
所有进程,按mpi分组根据任务分组,如果能整除就快,如果不能整除,就会等待。
多线程高性能调度计算这块了解不深入。主要工作是算法层面的加强。
查mpi底层怎么高性能调度?
1.1,你会怎么优化等待时间?
没得优化啊,这里必须等待这个任务完成才能下一步。
2、grpc,介绍一下。
2.2、定制两个模块使用grpc,交互?
实现。说下步骤,设计流程?
忘记说序列化,反序列化了,
微服务消费者
微服务动态代理
协议请求(序列化)
rpc客户端。
2、遇到的问题?怎么解决的?