1. Epoll、poll及select的区别
相同点:三者均能够提供多路I/O复用的解决方案,在linux平台上都可以支持。
不同点:
<1> Select的本质是设置或者检查存放文件句柄(fd)的标志位的数据结构来进行下一步处理。其缺陷是单个进程所打开的fd是有限制的,默认值是1024;并且对socket扫描是线性扫描,即采用轮询的方式,效率较低。当维护一个存放大量fd的数据结构时,这样会使得用户空间和内核空间在传递该结构时复制开销大。
<2> poll本质和select没有什么区别。Poll最大的特点是“水平触发”,如果报告了fd之后,没有被处理,下次poll会再次报告该fd。
<3> epoll支持水平触发和边缘触发(LT和ET),最大的特点在于边缘触发, 它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。没有最大并发连接的限制,能打开的fd远远大于1024。效率大大提升,不在是轮询方式,不会随着fd数目的增加效率下降。
2. 线程、进程、多线程、并发及并行的概念及区别。
线程:程序的一个执行。
进程:CPU的基本调度单位。
线程与进程之间的关系:
1> 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和基本调度单位;
2> 资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段、数据段及扩展段。但是每个线程拥有自己的运行时段,用来存放所有局部变量和临时变量;
3> 线程在执行过程中,需要协作同步。不同进程的线程要利用消息通信的方式实现同步。
多线程:多线程是多任务处理的一种特殊形式。多任务处理允许让电脑同时运行两个或两个以上的程序。
并发:指的是两个或多个独立的活动在同一时段内发生。(同一时间段内可以交替处理多个操作)
并行:指的是计算机在同一时刻,在某个时间点上处理两个或以上的操作。(注重同时性)
多线程与并发的区别:
并发与多线程之间的关系就是目的与手段之间的关系。多线程就是将原本可能是串行的计算改为并行的手段、途径或者模型,有时候我们称多线程编程也为并发编程。当然,目的与手段往往是一对多的关系。并发编程还有其它方式,例如函数式编程。
- 多线程变成下什么情况下需要加volitile关键字?
Volatile个关键字并不是用来解决多线程竞争问题,而是用来修饰一些因为程序不可控因素导致变化的变量,比如访问底层硬件设备的变量,以提醒编译器不要对该变量进行优化。
如果对共享变量使用volitile关键字,可能存在竞争的操作中不加锁或使用原子操作对解决多线程竞争并没有什么作用。这是因为volitile并不能保证操作的原子性,在读入、写入变量的过程中仍然可能被其他线程打断导致意外结果。所以不建议加volite关键字。
4. 多线程实现的方式?同步机制?
多线程实现有四种方式,分别是:
1> 继承QThread类,重写QThread::run()函数;
2> 继承QRunnable类(是所有可执行对象的基类),代表了由run()函数表示的一个任务或一段可执行的代码。使用该类通常情况下需要借助QThreadPool来再另一个独立的线程中执行该代码。如果QRunnable对象的autoDelete()设置为true的话,QRunnable会在run()娙结束后自动删除该对象(前提条件是必须要在QThreadPool::start()之前设置)。如果有用到信号槽,还得同时继承QObject类;
3> 继承QObject类,使用moveToRhread()方法;
4> 使用QtConcurrent::run()启动新线程。
同步机制:
线程之间通信的两个基本问题是互斥和同步
线程同步:是线程之间所具有的的一种制约方式,一个线程的执行依赖另一个线程的消息,当它没有等到另一个线程的消息时应等待,知道消息到达时才被唤醒。
线程互斥:是对共享的操作系统资源,在哥哥线程访问时的排他性。当有若干个线程都是用同一共享资源时,任何时刻只允许一个线程区使用,其它线程必须等待,直到占用资源释放后。
线程互斥是一种特殊的线程同步。实际上,互斥和同步对应着线程通信发生的两种情况:
a. 当有多个线程访问共享资源而不使资源被破坏时;
b. 当一个线程将某个任务已完成情况通知给另外一个线程或者多个线程时;
WIN32系统中,同步机制主要有一下4种:
1> 事件(Event)
2> 信号量(semaphore)
3> 互斥量(mutex)
4> 临界区(Critical section)
5. 如何理解互斥锁、条件锁(条件变量)、读写锁及自旋锁?
互斥锁:
用于保护临界区,以保证任何时刻都只有一个线程在执行临界区中的代码。如果多个线程在同一临界区内活动,就有可能产生竞态条件导致错误,其中包含递归锁(同一个线程多次获得该锁,别的线程必须等待该线程释放所有次数的锁才可获得)和非递归锁。最常见的互斥场景就是多个线程访问共享资源。
读写锁:
从广义上说,也可以认为是一种共享的互斥锁,可以多个线程进行读操作,但是写操作必须单独进行,不可多写、不可边读边写。
条件变量:
允许线程以一种无竞争的方式等待某个条件的发生。当该条件没有发生的时候,线程一直处于休眠状态。当被去他线程通知条件已经发生时,线程才会被唤醒从而继续进行下去。
自旋锁:
当要获取一把自旋锁的时候又被别的线程持有时,不断循环的去检索是否可以获得自旋锁,一直占CPU内存。
6. Socket套接字在多线程发送数据时需要加锁吗?
1> 对于UDP,多线程读写同一个socket不用加锁,不过更好的方法是每个线程都有自己的socket,避免竞争,可以用SO_REUSEPORT来实现这一点;
2> 对于TCP,通常多线程读写同一个socket是错误的设计,因为有short write的可能。假如加锁,而又发生short write,必须等到整条信息发送完才能解锁。
总而言之,对于UDP,加锁是多余的;对于TCP,加锁是错误的。
7. Fork与vfork区别?exit()与_exit() 区别?
Fork与vfork区别:
A. 前者是子进程拷贝父进程的数据段、代码段。相反,后者是子进程和父进程的共享数据段;
B. 前者父子进程的执行次序不确定,后者是先保证子进程先运行,在调用exec和exit之前与父进程是共享的,之后父进程数据才可被调用执行;
C. vfork()保证子进程先运行,在它调用exec和exit之后父进程才可能被调度运行。如果在调用两函数之前子进程依赖父进程的进一步动作,则会导致死锁。
exit()与_exit() 区别:
前者是在调用exit系统调用前要检查文件的打开情况,把文件缓冲区内的内容写会文件;而后者是直接退出进程,清除其内存空间。
8. 线程安全与线程不安全
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问知道该线程读取完毕,其他线程才可使用。不会出现数据不一致或者数据污染。
线程不安全就是不听数据访问保护,有可能出现多个线程先后更改数据造成数据被污染。
9. 死锁的四个必要条件?
死锁:多个并发进程因争夺系统资源而产生相互等待现象。
本质原因:系统资源有限;进程推进顺序不合理。
必要条件:互斥;占有且等待;不可抢占;循环等待。
10. 如何编写套接字?
套接字socket编程有三种方式,流式套接字、数据报套接字及原始套接字。基于TCP的socket编程采用流式套接字。
服务器端一般步骤:
1> 创建一个socket,用函数socket();
2> 设置socket属性,用函数setsockopt();
3> 绑定IP地址、端口等信息到socket上,用函数bind();
4> 开启监听,用函数listen();
5> 接收客户端上来的连接,用函数accept();
6> 收发数据,用函数send()和recv(),或者read()和write();
7> 关闭网络连接
8> 关闭监听。
客户端一般步骤:
1> 创建一个socket,用函数socket(); 2> 设置socket属性,用函数setsockopt();* 可选 3> 绑定IP地址、端口等信息到socket上,用函数bind();* 可选 4> 设置要连接的对方的IP地址和端口等属性; 5> 连接服务器,用函数connect(); 6> 收发数据,用函数send()和recv(),或者read()和write(); 7> 关闭网络连接;
11. 说一下TCP三次握手、四次挥手全过程理解
TC三次握手四次挥手全过程理解
12. Main函数执行前后还执行什么代码?
Main函数执行之前,主要是初始化系统相关资源:
1> 设置栈指针;
2> 初始化static静态和global全局变量;
3> 将未初始化的全局变量赋初值;
4> 运行全局构造器;
Main函数执行之后:
1> 全局对象的析构函数会在main函数之后运行。
13. C++如何阻止一个类被实例化?一般在什么时候将构造函数声明为private?
1> 将类定义为抽象类或者将构造函数声明为private;
2> 不允许类外部创建类对象,只能内部创建对象。
14. STL中的vector容器是如何扩容的?
1> 计算容器大小的函数:
Size(): 返回当前vector元素的个数;
Capacity() : 返回当前vector中最大存储元素的个数。
2> 产生扩容的情况
A. 当添加元素时导致扩容;
B. 使用reserve或者resize函数导致的扩容(前者扩大的只是容器的预留空间,空间内不正真创建元素对象;后者改变容器大小并创建对象)
3> 容器的回收
使用clear()和erase()两个函数只是清空元素,并不回收内存;
先使用clear()在使用swap(),释放空间并回收内存。
15. Vector中的earse()函数在使用的时候需要注意什么?
Vector::earse()表示从指定容器删除指定位置的元素或某段范围内的元素。
如果是从指定容器删除指定位置的元素,那么返回值是一个迭代器,指向被删除元素的下一个元素;
如果是从指定容器删除某段范围内的元素时,返回值也是一个迭代器,指向最后一个删除元素的下一个元素。
16. C++中内存泄漏的几种情况以及解决方法?
1> 在类的构造函数和析构函数中没有匹配的调用new和delete;
2> 没有正确的清楚嵌套的对象指针;
3> 在释放对象数组时,delete未加方括号;
4> 指向对象的指针数组不等同于对象数组(解决方法,通过一个循环将每个对象释放了,然后再把指针释放了);
5> 缺少拷贝构造函数(按值传递会调用拷贝构造函数,引用传递不会调用)
6> 缺少重载赋值运算符;
7> 没有将基类的析构函数定义为虚函数。
17. 堆栈溢出的原因以及解决方法?
栈溢出是由于C语言系列没有内置检查机制来确保复制到缓冲区的数据不得大于缓冲区的大小,因此数据足够大时,将会溢出缓冲区的范围;
堆溢出的产生是由于过多的函数调用,导致调用堆栈无法容纳这些调用的返回地址,一般在递归中产生。
18. TCP与UDP的区别?
1> 基于连接与无连接;
2> 对系统资源的要求TCP较多,UDP较少;
3> UDP程序结构较简单;
4> 流模式与数据报模式;
5> TCP保证数据的正确性,UDP可能丢失,TCP保证数据顺序,UDP不保证。
19. 为什么构造函数不能声明为虚函数?
1> 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象 的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。。。 2> 虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初 始化,将无法进行。
20. 多线程情况下,Qt中的信号槽分别在什么线程中执行,如何控制?
通过connect函数的第五个参数connectType来控制。 connect用于连接qt的信号和槽,在qt编程过程中不可或缺。它其实有第五个参数,只是一般使用默认值,在满足某些特殊需求的时候可能需要手动设置。 Qt::AutoConnection: 默认值,使用这个值则连接类型会在信号发送时决定。如果接收者和发送者在同一个线程,则自动使用Qt::DirectConnection类型。如果接收者和发送者不在一个线程,则自动使用Qt::QueuedConnection类型。 Qt::DirectConnection:槽函数会在信号发送的时候直接被调用,槽函数运行于信号发送者所在线程。效果看上去就像是直接在信号发送位置调用了槽函数。这个在多线程环境下比较危险,可能会造成奔溃。 Qt::QueuedConnection:槽函数在控制回到接收者所在线程的事件循环时被调用,槽函数运行于信号接收者所在线程。发送信号之后,槽函数不会立刻被调用,等到接收者的当前函数执行完,进入事件循环之后,槽函数才会被调用。多线程环境下一般用这个。 Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。 Qt::UniqueConnection:这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是避免了重复连接。