本文已参与「新人创作礼」活动,一起开启掘金创作之路。
1. 说说多线程和多进程的不同
(1)一个线程从属于一个进程;一个进程可以包含多个线程。
(2)一个线程挂掉,对应的进程挂掉,多线程也会挂掉;一个进程挂掉不会影响其他进程,多进程稳定。
(3)进程系统开销显著大于线程开销;线程需要的系统资源更少。
(4)多个进程在执行时拥有各自独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展段;但每个线程拥有自己的栈段和寄存器组。
(5)多进程切换时需要TLB获取新的地址空间,然后切换硬件上下文和内核栈;而多线程切换时只需要切换硬件上下文和内核栈。
(6)通信方式不一样。多线程的通信方式有临界区、互斥量、信号量、条件变量、读写锁;多进程的通信方式有管道、系统PIC(包括消息队列、信号量、信号、共享内存)、套接字socket。
(7)多进程适应于多核、多机分布;多线程适用于多核。
2. 简述互斥锁的机制,互斥锁与读写锁的区别
互斥锁机制:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。
互斥锁与读写锁: (1)读写锁区分读者和写者,而互斥锁不区分;
(2)互斥锁同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。
互斥锁原理:
互斥锁其实就是一个bool型变量,为true时表示锁可获取,为false时表示已上锁。这里的互斥锁其实是泛指Linux所有的锁机制。
我们采用互斥锁保护临界区,从而防止竞争条件。也就是说,一个线程在进入临界区时应得到锁;它在退出临界区时释放锁。函数acquire()获取锁,而函数release()释放锁,如下面的伪代码:
do {
获得锁
关键区
释放锁
剩余区
} while(true);
每个互斥锁都有一个布尔变量available,它的值表示锁是否可用。如果锁可用,那么调用acquire()会成功,并且锁不再可用。当一个线程试图获取不可用的锁时,它会阻塞,知道锁被释放。
如下定义acquire():
acquire() {
while (!available);
/* busy wait */
available = false;
}
如下定义release():
release() {
available = true;
}
3. 说说什么是信号量,有什么作用?
概念
信号量本质上是一个计数器,用于多进程对共享数据对象的读取,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只能由一个进程独享。
原理
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),具体的行为如下:
(1)P(sv)操作
如果sv的值大于0,就给它减1;如果它的值为0,就挂起该进程的执行(信号量的值为正,进程获得该资源的使用权,进程将信号量减1,表示它使用了一个资源单位)。
(2)V(sv)操作
如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待而挂起,就给它加1(若此时信号量的值为0,则进程进入挂起状态知道信号量的值大于0,若进程被唤醒则返回第一步)。
作用
用于多进程对共享数据对象的读取,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。
4. 进程、线程的中断切换过程是怎么样的?
上下文切换指的是内核(操作系统的核心)在CPU上对进程或者线程进行切换。
进程上下文切换
(1)保护被中断进程的处理器的现场信息; (2)修改被中断进程的进程控制块有关信息,如进程状态等; (3)把被中断进程的进程控制块加入有关队列; (4)选择下一个占有处理器运行的进程; (5)根据被选中进程设置操作系统用到的地址转换和存储保护信息;
切换页目录以使用新的地址空间(TLB) 切换内核栈和硬件上下文(包括分配的内存,数据段,堆栈段等)
(6)根据被选中进程恢复处理器现场
线程上下文切换
(1)保护被中断线程的处理器的现场信息; (2)修改被中断线程的线程控制块有关信息,如线程状态等; (3)把被中断线程的线程控制块加入有关队列; (4)选择下一个占有处理器运行的线程; (5)根据被选中线程设置操作系统用到的存储保护信息;
切换内核栈和硬件上下文(切换堆栈,以及各寄存器)
(6)根据被选中线程恢复处理器现场。
5. 简述自旋锁和互斥锁的使用场景
(1)互斥锁用于临界区持锁时间比较长的操作,比如下面这种情况都可以考虑 a.临界区有IO操作 b.临界区代码复杂或者循环量大 c.临界区竞争非常激烈 d.单核处理器
(2)自旋锁就主要用在持锁时间非常短且CPU资源不紧张的情况下。
6. 多线程和单线程有什么区别,多线程编程需要注意什么,多线程加锁需要注意什么?
区别
(1)多线程从属于一个进程,单线程也从属于一个进程;一个线程挂掉都会导致从属的进程挂掉。
(2)一个进程里有多个线程,可以执行多个任务;一个进程里只有一个线程就只能执行一个任务。
(3)多线程并发执行多任务,需要切换内核栈与硬件上下文,有切换的开销;单线程不需要切换,没有切换的开销。
(4)多线程并发执行多任务,需要考虑同步的问题;单线程则不用考虑。
多线程编程需要考虑的问题
多线程编程需要考虑同步的问题。线程间的同步方式包括互斥锁、信号量、条件变量、读写锁。
多线程加锁需要注意什么
多线程加锁需要注意死锁的问题。破坏死锁的必要条件来避免死锁。
7. 说说sleep和wait的区别
sleep
sleep是一个延时函数,让进程或线程进入休眠。休眠完毕后继续运行。
在linux下,sleep函数的参数是毫秒 例如:
#include <windows.h>// 首先应该先导入头文件
//注意第一个字母是大写。 //就是到这里停半秒,然后继续向下执行。
Sleep (500) ;
在linuxC语言中sleep的单位是秒 例如:
#include <unistd.h>// 首先应该先导入头文件
//停5秒 //就是到这里停5秒,然后继续向下执行。
sleep(5);
wait
wait是父进程回收子进程PCB资源的一个系统调用。进程一旦调用了wait函数,就立即阻塞本身,然后由wait函数自动分析当前进程的某个子进程是否已经退出,当找到一个已经变成僵尸的子进程时,wait就会手机这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞,直到有一个出现为止。
函数原型:
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
子进程的结束状态值会由参数statues返回,而子进程的进程识别码也会一起返回。如果不需要结束状态值,参数status可以设成NULL。
区别:
(1)sleep是一个延时函数,让进程或线程进入休眠。休眠完毕后继续运行。
(2)wait是父进程回收子进程PCB(Process Control Block)资源的一个系统调用。
8. 说说线程池的设计思路,线程池中线程的数量由什么来确定
设计思路:
(1)设置一个生产者消费者队列,作为临界资源;
(2)初始化n个线程,并让它们运行起来,加锁区队列里取任务运行;
(3)当任务队列为空时,所有线程阻塞。
(4)当生产者队列来了一个任务后,先对队列加锁,把任务挂到队列上,然后使用条件变量去通知阻塞中的一个线程来处理。
线程池中线程数量:
线程数量和哪些因素有关:CPU、IO、并行、并发 如果是CPU密集型应用,则线程池大小设置为:CPU数目 + 1;
如果是IO密集型应用,则线程池大小设置为:2*CPU数目 + 1;
最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目
所以线程等待时间比越高,线程需要越多;线程CPU时间所占比例越高,线程需要越少。
为什么要创建线程池:
创建线程和销毁线程的花销是比较大的,这些时间有可能比处理业务所花的时间还要多,频繁的创建线程和销毁线程,再加上业务工作流程,消耗系统资源的时间,可能会导致系统资源不足。同时线程池也是为了提升系统效率而被创造出来的。
线程池的核心线程和普通线程:
任务队列可以存放100个任务,此时为空,线程池里有10个核心线程,若突然来了10个任务,那么刚好10个核心线程直接处理;若又来了90个任务,此时核心线程来不及处理,那么有80个任务先入队列,再创建核心线程处理任务;若又来了120个任务,此时任务队列已满,不得已,就得创建20个普通线程来处理多余的任务。 以上是线程池的工作流程。
9. 进程和线程相比,为什么慢?
(1)进程系统开销比线程大;线程所需要的系统资源更少。 (2)进程切换开销比线程大。多进程切换时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈;多线程切换时只需要切换上下文和内核栈。 (3)进程通信比线程通信所花费的开销大。进程通信需要借助管道、消息队列、共享内存、需要额外申请空间,通信繁琐;而线程共享进程的内存,如代码段、数据段、扩展段,通信快捷简单,同步开销更小。
10. 简述Linux零拷贝的原理
什么是零拷贝:
所谓【零拷贝】,描述的是计算机操作系统中,CPU不执行将数据从一个内存区域,拷贝到另一个内存区域的任务。通过网络传输文件时,可以节省CPU周期和内存带宽。
零拷贝的好处:
(1)节省了CPU周期,空出的CPU可以完成更多其他的任务; (2)减少了内存区域之间数据的拷贝,节省内存带宽; (3)减少用户态和内核态之间数据的拷贝,提升数据传输效率; (4)应用零拷贝技术,减少用户态和内核态之间的上下文切换。
零拷贝原理:
在传统的IO中,用户态与内核态空间之间的复制时完全不必要的,因为用户态空间仅仅只起到了一种数据转存媒介的作用,除此之外,没有做任何事情。
(1)Linux 提供了 sendfile() 用来减少我们的数据拷贝和上下文切换次数。
a. 发起 sendfile() 系统调用,操作系统由用户态空间切换到内核态空间(第一次上下文切换)
b. 通过 DMA 引擎将数据从磁盘拷贝到内核态空间的输入的 socket 缓冲区中(第一次拷贝)
c. 将数据从内核空间拷贝到与之关联的 socket 缓冲区(第二次拷贝)
d. 将 socket 缓冲区的数据拷贝到协议引擎中(第三次拷贝)
e. sendfile() 系统调用结束,操作系统由用户态空间切换到内核态空间(第二次上下文切换)
根据以上过程,一共有 2 次的上下文切换,3 次的 I/O 拷贝。我们看到从用户空间到内核空间并没有出现数据拷贝,从操作系统角度来看,这个就是零拷贝。内核空间出现了复制的原因: 通常的硬件在通过DMA访问时期望的是连续的内存空间。 (2)mmap数据零拷贝原理 如果需要对数据做操作,Linux提供了mmap的零拷贝来实现。