2020-10-20 金山云三面

1.介绍一个最有技术深度(你完成的)的项目经历?

负责sdk-demo展示系统的后端开发
功能需求:用于向产品经理、商务同事使用的SDK Demo演示的一个web端,用户可以在网页上添加图片提交检测,后台将调用对应配置的SDK进行检测,返回检测结果。

系统架构:后端是worker+api后端,worker是对各SDK单独封装的图片检测的worker服务,通过grpc接口提供图片检测服务,api后端向前端提供HTTP接口,使用python3的Flask框架实现。

负责的工作:主要包括对sdk封装worker以及api后端的开发工作,其中worker使用C++开发,api后端使用python3的Flask框架进行开发。服务通过docker部署。

对各SDK需要单独封装worker,通过grpc接口向外提供服务,(单独封装的原因有两个:一是将所有SDK打包在一个worker时,有多个SDK运行时存在依赖库冲突,不能同时运行;二是服务器的GPU显存有限,一般需要将各SDK分散在不同机器上运行)。

api后端通过grpc发起对worker的调用。


2.介绍下socket半关闭状态?

  1. 怎样表示输入或输出的结束

    1. 都知道TCP通信其实就是用客户端(Socket对象)的socket传递数据的,传递数据都是通过Socket类获取InputStream和OutputStream的;

    2. 一般最纠结的就是不知道对方的输出什么时候结束,前面给出的示例都没有处理输出结束(对方)的问题,除非你直接关闭程序否则输出永远都不会结束;

    3. 都知道只要关闭输出流就表示输出结束,但是在Socket通信中,一旦输出流被彻底关闭就意味着Socket的关闭,那么网络通信就无法继续了,通常希望关闭输出流之后输入流还可以继续从远程接受数据,或者关闭输入流之后还能通过输出流向远程传送数据;

    4. 因此Socket类提供了半关闭的方法:
      i. void shutdownOutput(); // 只关闭输出流(但输入流仍然能用)进入半写状态(write-half),通知对方远程输出已经关闭,

      ii. void shutdownInput(); // 只关闭输入流(但输出流仍然能用)进入半读状态(read-half),通知对方远程读取已经关闭

    5. 同样Socket类也提供了查看是否处于半读半写状态的方法:

      i. boolean isOutputShutdown(); // 是否处于半写状态

      ii. boolean isInputShutdown(); // 是否处于半读状态

注意:即使先后调用这两个方法也不会关闭Socket,只不过处于既不能输出也不能输入的废物状态;

上面的方法是半关闭方法,那么彻底关闭输出输入流应该怎么做呢?
a. 通常会对Socket对象调用getInputStream、getOutputStream然后用BufferedReader、PrintStream等包装;
b. 那个包装器就是彻彻底底的输入输出流了,如果调用包装器对象br、ps的close方法那就彻底的关闭输入输出流了;
c. 它们的关闭就意味着Socket也被关闭了!!

检查Socket对象是否关闭的方法:boolean Socket.isClosed();

  1. 半关闭适用于哪些场景

    1. 当然是用于那些不需要长久保持通信状态的一站式服务;

    2. 特别适用于HTTP通信,而且特别适用于客户端,因为客户端在发送请求后就无需再发送其它数据了,往往只是等待服务器端返回想要的资源;

    3. 因此在客户端发送完请求之后就可以立马半关闭输出流了(半写状态).

https://blog.csdn.net/qq_23013625/article/details/55211362


3.socket设置了non-blocking,此时触发connect(),会立马返回吗,返回内容有哪些?

设置socket为非阻塞non-blocking
使用socket()创建的socket(file descriptor),默认是阻塞的(blocking);connect()、accept()\读写函数、gethostbyname()都得阻塞函数。

使用函数fcntl()(file control)可设置创建的socket为非阻塞的non-blocking。如果指定非阻塞模式,程序调用可能造成阻塞的函数时,如果会发生阻塞,这些函数返回-1并将errno设置为EAGAIN或EWOULDBLOCK,程序可继续向下运行。

non-blocking和select结合使用
select通过轮询,监视指定file descriptor(包括socket)的变化,知道:哪些ready for reading, 哪些ready for writing,哪些发生了错误等。select和non-blocking结合使用可很好地实现socket的多client同步通信。

这样使用原本blocking的各种函数,可以立即获得返回结果。通过判断返回的errno了解状态:

accept():
在non-blocking模式下,如果返回值为-1,且errno == EAGAIN或errno == EWOULDBLOCK表示no connections没有新连接请求;

recv()/recvfrom():
在non-blocking模式下,如果返回值为-1,且errno == EAGAIN表示没有可接受的数据或很在接受尚未完成;

send()/sendto():
在non-blocking模式下,如果返回值为-1,且errno == EAGAIN或errno == EWOULDBLOCK表示没有可发送数据或数据发送正在进行没有完成。

read/write:
在non-blocking模式下,如果返回-1,且errno == EAGAIN表示没有可读写数据或可读写正在进行尚未完成。

connect():
在non-bloking模式下,如果返回-1,且errno = EINPROGRESS表示正在连接。

特别地:返回值<0时并且(errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情况下认为连接是正常的,继续接收。只是阻塞模式下recv会阻塞着接收数据,非阻塞模式下如果没有数据会返回,不会阻塞着读,因此需要循环读取)。

返回说明: 成功执行时,返回接收到的字节数。另一端已关闭则返回0。失败返回-1,errno被设为以下的某个值

EAGAIN:套接字已标记为非阻塞,而接收操作被阻塞或者接收超时
EBADF:sock不是有效的描述词
ECONNREFUSE:远程主机阻绝网络连接
EFAULT:内存空间访问出错
EINTR:操作被信号中断
EINVAL:参数无效 ENOMEM:内存不足
ENOTCONN:与面向连接关联的套接字尚未被连接上
ENOTSOCK:sock索引的不是套接字 当返回值是0时,为正常关闭连接;

http://blog.sina.com.cn/s/blog_4b6f784001012e3z.html


4.有用过epoll吗?

关于epoll的示例:https://www.cnblogs.com/aicro/archive/2012/12/27/2836170.html


5.说出下面类的长度 sizeof(A)?

class A{
    
    
	int a;
	static int b;
	static void fun_1(){
    
    }
	virtual void fun_2(){
    
    }
};

64位系统运行,输出a长度是16字节,


6.C++ 中关键字explicit的作用?

explicit用来防止由构造函数定义的隐式转换。

要明白它的作用,首先要了解隐式转换:可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。

例如:

class things
{
    
    
    public:
        things(const std::string&name =""):
              m_name(name),height(0),weight(10){
    
    }
        int CompareTo(const things & other);
        std::string m_name;
        int height;
        int weight;
};

这里things的构造函数可以只用一个实参完成初始化。所以可以进行一个隐式转换,像下面这样:

things a;
................//在这里被初始化并使用。
std::string nm ="book_1";
//由于可以隐式转换,所以可以下面这样使用
int result = a.CompareTo(nm);

这段程序使用一个string类型对象作为实参传给things的CompareTo函数。这个函数本来是需要一个tings对象作为实参。现在编译器使用string nm来构造并初始化一个

things对象,新生成的临时的things对象被传递给CompareTo函数,并在离开这段函数后被析构。

这种行为的正确与否取决于业务需要。假如你只是想测试一下a的重量与10的大小之比,这么做也许是方便的。但是假如在CompareTo函数中还涉及到了要除以初始化为0的height属性,那么这么做可能就是错误的。需要在构造tings之后更改height属性不为0。所以要限制这种隐式类型转换。

那么这时候就可以通过将构造函数声明为explicit,来防止隐式类型转换。

explicit关键字只能用于类内部的构造函数声明上,而不能用在类外部的函数定义上。

google的c++规范中提到explicit的优点是可以避免不合时宜的类型变换,缺点无。所以google约定所有单参数的构造函数都必须是显示的,只有极少数情况下拷贝构造函数可以不声明称explicit。例如作为其他类的透明包装器的类。

effective c++中说:被声明为explicit的构造函数通常比其non-explicit兄弟更受欢迎。因为它们禁止编译器执行非预期(往往也不被期望)的类型转换。除非我有一个好理由允许构造函数被用于隐式类型转换,否则我会把它声明为explicit。

https://www.cnblogs.com/vanwoos/p/5364656.html


7.多线程同步锁用过哪些锁?

c++ 线程同步机制

互斥量

在使用互斥量的时候,最好使用RAII进行封装,使用非递归的互斥量,尽量在一个函数内进行lock、unlock。

常有的互斥量对象,简单的互斥对象std::mutex,带有超时机制的互斥对象std::timed_mutex,一般使用RAII来避免死锁的情况。

std::lock_guard,对象生存期内是不允许手动加锁解锁的。构造时可选是否加锁(不加锁时假定当前线程已经获得锁的所有权),析构时自动释放锁,所有权不可转移。

std::unique_lock,对象生存期可以进行手动加锁解锁。比lock_guard更加灵活。

lock_guard只支持std::lock和std::adopt_lock。

unique_lock则支持:std:lock、std::defer_lock、std::try_lock、std::adopt_lock。

defer_lock 不取得互斥量的所有权;当多个线程都使用同样N个互斥量的时候,必须保证其加锁的顺序是一致的,这种情况下使用try_lock更好。

try_lock 则会在没有阻塞的时候取得互斥量的所有权;其变种try_lock_for(duration)和try_lock_until(timepoint)。

adopt_lock 即使互斥量已经被另外的线程加锁,也会夺取互斥量的所有权进而在该线程加锁。

条件变量

条件变量,是一个或多个线程等待某个布尔表达式为真,即等待别的线程“唤醒”它。

对于wait()端,为了防止虚假唤醒,必须配合锁一起使用:

  1. 必须与mutex 一起使用,该布尔表达式的读写需受此mutex 保护

  2. 在mutex 已上锁的时候才能调用wait()

  3. 把判断布尔条件和wait() 放到while 循环中

示例:

MutexLock mutex;
Condition cond(mutex);
std::deque<int> queue;
 
int dequeue()
{
    
    
  MutexLockGuard lock(mutex);
  while (queue.empty()) {
    
      // 必须用循环;必须在判断之后再 wait()
    cond.wait(); // 这一步会原子地 unlock mutex 并进入 blocking,不会与 enqueue 死锁
  }
  assert(!queue.empty());
  int top = queue.front();
  queue.pop_front();
  return top;
}

对于 signal/broadcast 端:

  1. 不一定要在mutex 已上锁的情况下调用signal

  2. 在signal 之前一般要修改布尔表达式

  3. 修改布尔表达式通常要用mutex保护

void enqueue(int x)
{
    
    
  MutexLockGuard lock(mutex);
  queue.push_back(x);
  cond.notify();
}

实现了一个简单的unbounded BlockingQueue。

条件变量是较底层的同步原语,很少直接使用,一般都是用它来实现高层的同步措施,如 BlockingQueue 或CountDownLatch

临界区

临界区是值一个访问共享资源的代码段。当有一个线程访问了临界区之后,其他线程想要访问临界区时会被挂起,直到进入临界区的线程离开。windows API提供了临界区对象结构体CRITICAL_SECTION,常用API有:

  1. 申请一个临界区变量 CRITICAL_SECTION gSection;

2.InitializeCriticalSection(&gSection),初始化临界区,唯一的参数是指向结构体CRITICAL_SECTION的指针变量(LPCRITICAL_SECTION lpCriticalSection)。

3.EnterCriticalSection(&gSection),线程进入已经初始化的临界区,并拥有该临界区的所有权。这是一个阻塞函数,如果线程获得临界区的所有权成功,则该函数将返回,调用线程继续执行,否则该函数将一直等待,这样会造成该函数的调用线程也一直等待。如果不想让调用线程等待(非阻塞),则应该使用TryEnterCriticalSection(&gSection)。

4.LeaveCriticalSection(&gSection),线程离开临界区并释放对该临界区的所有权,以便让其他线程也获得访问该共享资源的机会。一定要在程序不适用临界区时调用该函数释放临界区所有权,否则程序将一直等待造成程序假死。

5.DeleteCriticalSection(&gSection),该函数的作用是删除程序中已经被初始化的临界区。如果函数调用成功,则程序会将内存中的临界区删除,防止出现内存错误。

https://blog.csdn.net/forever917/article/details/59108853?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2alltop_click~default-1-59108853.nonecase&utm_term=c++%E7%BA%BF%E7%A8%8B%E9%94%81%E7%9A%84%E5%90%8C%E6%AD%A5%E6%9C%BA%E5%88%B6&spm=1000.2123.3001.4430


9.介绍智能指针?多线程同时创建、销毁shared_ptr会出问题吗?

C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。
为什么要使用智能指针:

智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

1. auto_ptr(c++98的方案,cpp11已经抛弃)

采用所有权模式。

auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错.

此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!

2. unique_ptr(替换auto_ptr)

unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。

采用所有权模式,还是上面那个例子

unique_ptr<string> p3 (new string ("auto"));   //#4
unique_ptr<string> p4;                       //#5
p4 = p3;//此时会报错!!

编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。

另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;                                      // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。

注:如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。例如:

unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;

3. shared_ptr

shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。

成员函数:

use_count 返回引用计数的个数

unique 返回是否是独占所有权( use_count 为 1)

swap 交换两个 shared_ptr 对象(即交换所拥有的对象)

reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少

get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr sp(new int(1)); sp 与 sp.get()是等价的

4. weak_ptr

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。

weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

https://www.nowcoder.com/tutorial/93/a34ed23d58b84da3a707c70371f59c21

shared_ptr是线程安全的吗?

shared_ptr的reference count是线程安全的,但是指向的对象不是线程安全的!

智能指针的引用计数在手段上使用了atomic原子操作,只要在shared_ptr在拷贝或赋值时增加引用,析构时减少引用就可以了。

首先源自操作是线程安全的,所有智能指针在多线程下引用计数也是安全的,也就是说智能指针在多线程下传递使用时引用计数是不会有线程安全问题的,但是这能真正的保证shared_ptr指针的线程安全问题吗。

虽然通过原子操作解决了引用计数的计数的线程安全问题, 但是智能指针指向的对象的线程安全问题,智能指针没有做任何的保证。 首先智能指针有两个变量,一个是指向的对象的指针,还有一个就是我们上面看到的引用计数管理对象, 当智能指针发生拷贝的时候,标准库的实现是县拷贝智能指针,再拷贝引用计数对象(拷贝引用计数对象的时候,会使use_count加一),这两个操作并不是原子的,隐患就出现在这里

在这里插入图片描述
这里陈硕老师说道:“这正是多线程读写同一个shared_ptr必须枷锁的原因”, 为了保证程序的绝对的安全是没错的, 但也不是绝对,上面的情景是特殊场景,这种场景也只是为了说明问题而已,真正开发过程中不一定会用到此场景。其实这个问题的根本还是上面说的智能指针指向的对象的线程安全,shared_ptr没有做任何保证,上面的情景就打破了这一准则,在赋值的过程中,改变了shard_ptr指向的对象的内容,甚至不只是修改了对象这么简单,上面的情景直接把智能指针指向的对象给换了。这中情况不用想肯定会出问题。

如果你能保证不会有多个线程同时修改或替换指针指向的对象,不用加锁是完全没有问题的,或者说指针指向的对象本身已经是线程安全(包括多线程下的读写安全和构造析构安全)。总之一句话智能指针指向的对象的线程安全,标准库是没有保证的。


10.map为什么用红黑树?跟平衡树最大区别在哪里?

红黑树和AVL树都是最常用的平衡二叉搜索树,它们的查找、删除、修改都是O(lgn) time

AVL树和红黑树有几点比较和区别:
(1)AVL树是更加严格的平衡,因此可以提供更快的查找速度,一般读取查找密集型任务,适用AVL树。
(2)红黑树更适合于插入修改密集型任务。
(3)通常,AVL树的旋转比红黑树的旋转更加难以平衡和调试。

总结:
(1)AVL以及红黑树是高度平衡的树数据结构。它们非常相似,真正的区别在于在任何添加/删除操作时完成的旋转操作次数。

(2)两种实现都缩放为a O(lg N),其中N是叶子的数量,但实际上AVL树在查找密集型任务上更快:利用更好的平衡,树遍历平均更短。另一方面,插入和删除方面,AVL树速度较慢:需要更高的旋转次数才能在修改时正确地重新平衡数据结构。

(3)在AVL树中,从根到任何叶子的最短路径和最长路径之间的差异最多为1。在红黑树中,差异可以是2倍。

(4)两个都给O(log n)查找,但平衡AVL树可能需要O(log n)旋转,而红黑树将需要最多两次旋转使其达到平衡(尽管可能需要检查O(log n)节点以确定旋转的位置)。旋转本身是O(1)操作,因为你只是移动指针。

红黑树模拟:https://www.cs.usfca.edu/~galles/visualization/RedBlack.html

AVL树模拟:https://www.cs.usfca.edu/~galles/visualization/AVLtree.html

https://blog.csdn.net/21aspnet/article/details/88939297

猜你喜欢

转载自blog.csdn.net/weixin_43202635/article/details/109184157