概念
进程是对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的并发;
线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发;线程是操作系统可识别的最小执行和调度单位。每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。
线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。
进程与线程的区别
- 进程是资源分配的最小单位,线程是程序执行的最小单位
- 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。
而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。 - 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。
- 多进程程序更健壮,多线程程序只要有一个线程死掉,可能整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
多线程其中一个线程挂了,是否会影响进程?
如果子线程的崩溃是由于自己的独立栈空间引起的,那就不会对主线程和其他子线程产生影响,但是如果子线程的崩溃是因为对共享区域造成了破坏,那么大家就一起崩溃了。
线程有哪两种?
- 用户级线程(
user level thread
):对于这类线程,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在。在应用程序启动后,操作系统分配给该程序一个进程号,以及其对应的内存空间等资源。应用程序通常先在一个线程中运行,该线程被成为主线程。在其运行的某个时刻,可以通过调用线程库中的函数创建一个在相同进程中运行的新线程。用户级线程的好处是非常高效,不需要进入内核空间,但并发效率不高。 - 内核级线程(
kernel level thread
):对于这类线程,有关线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只能调用内核线程的接口。内核维护进程及其内部的每个线程,调度也由内核基于线程架构完成。内核级线程的好处是,内核可以将不同线程更好地分配到不同的CPU,以实现真正的并行计算。
线程之间的通信方式
多个线程读写同一个资源,容易引起冲突。
- 临界区:
通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问; - 互斥量 Synchronized/Lock:
采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。 - 信号量 Semphare:(同步和互斥)
为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。
非负的整数计数器,用来控制公共资源的访问。
当信号量值大于 0 时,则可以访问,否则将阻塞。PV 原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1。 - 事件/信号 Wait/Notify:
通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
wait()、sleep()、notify()、notifyall().
进程之间的通信方式
详细介绍进程之间的通信
进程间通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存等)、以及套接字socket。
1. 管道:
管道主要包括无名管道和命名管道:管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
1.1 普通管道PIPE:
1)它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端
2)它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)
3)它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
1.2 命名管道FIFO:
1)FIFO可以在无关的进程之间交换数据
2)FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
2.系统IPC:
2.1 消息队列
消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标记。 (消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点)具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息;
特点:
1)消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
2)消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
3)消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
2.2 信号量semaphore
信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
1)信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
2)信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
3)每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
4)支持信号量组。
2.3 信号signal
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
2.4 共享内存(Shared Memory)
它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
特点:
1)共享内存是最快的一种IPC,因为进程是直接对内存进行存取
2)因为多个进程可以同时操作,所以需要进行同步
3)信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
3. 套接字SOCKET:
socket也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机之间的进程通信。
互斥锁、自旋锁、读写锁
- 互斥锁
一个线程获得资源的使用权后,就会加锁,使用完解锁。使用过程中,其他线程想要获取该资源的锁,会被阻塞陷入睡眠状态,直到该资源被解锁才会被唤醒。 - 读写锁
拥有读锁、写锁、不加锁,3个状态
只有一个线程占用写锁,多个线程可用读锁。这也是它可以实现高并发的原因。
读写锁非常适合资源的读操作远多于写操作的情况。 - 自旋锁
特殊的互斥锁,当一个资源被一个线程加锁后,其他线程想要加锁,不会被阻塞睡眠而是陷入循环等待状态。循环检查资源持有者是否释放了资源,减少了线程从睡眠到唤醒的资源损耗,但是会一直占用CPU资源。
适用于:资源的锁被持有的时间短(或者说小于2次上下文切换的时间),而又不希望花资源在线程的唤醒上。
区域锁 boost::mutex::scoped_lock
在作用域内有效,当离开作用域自动释放锁,传递参数是锁。
区域锁就是把锁封装到一个对象里面。锁的初始化放到构造函数,锁的释放放到析构函数。这样当锁离开作用域时,析构函数会自动释放锁。
死锁
死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的下相互等待的现象。
死锁发生的四个必要条件如下:
- 互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源;
- 请求和保持条件:进程获得一定的资源后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但该进程不会释放自己已经占有的资源;
- 不可剥夺条件:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放;
- 环路等待条件:进程发生死锁后,必然存在一个进程-资源之间的环形链。
解决死锁的方法即破坏上述四个条件之一,主要方法如下:
- 资源一次性分配,从而剥夺请求和保持条件
- 可剥夺资源:即当进程新的资源未得到满足时,释放已占有的资源,从而破坏不可剥夺的条件
- 资源有序分配法:系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,释放则相反,从而破坏环路等待的条件
单核机器上写多线程程序,是否需要考虑加锁
在单核机器上写多线程程序,仍然需要线程锁。因为线程锁通常用来实现线程的同步和通信。在单核机器上的多线程程序,仍然存在线程同步的问题。因为在抢占式操作系统中,通常为每个线程分配一个时间片,当某个线程时间片耗尽时,操作系统会将其挂起,然后运行另一个线程。如果这两个线程共享某些数据,不使用线程锁的前提下,可能会导致共享数据修改引起冲突。
协程
协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
用户态:
协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。
协程不是进程也不是线程,而是一个特殊的函数,这个函数可以在某个地方挂起,并且可以重新在挂起处外继续运行。所以说,协程与进程、线程相比并不是一个维度的概念。
上下文切换:
- 进程、线程的切换者都是操作系统,根据操作系统自己的切换策略,用户不知道。切换过程是用户态--内核态--用户态。
- 协程的切换者是用户态,协程的切换内容是硬件上下文,切换内存保存在用户自己的变量(用户栈或堆)中。
优势:
那和多线程比,协程最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
一般情况下线程共享资源与独享资源的划分
进程切换与线程切换
进程切换分两步:
1.切换页目录以使用新的地址空间(线程不需要)
2.切换内核栈和硬件上下文
所以,进程上下文切换与线程上下文切换最主要的区别就是线程的切换虚拟空间内存是相同的(因为都是属于自己的进程),但是,进程切换的虚拟空间内存则是不同的。
同时,这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
为什么虚拟地址切换很慢?
把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个cache就是TLB,Translation Lookaside Buffer。
由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要进行切换,页表切换后TLB就失效了,cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢。
多线程是如何实现的?
主要是CPU通过给每个线程分配CPU时间片来实现多线程的。即使是单核处理器(CPU)也可以执行多线程处理。
时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程时同时执行的,时间片一般是几十毫秒(ms)。
进程挂了共享内存是否还存在,为什么?
进程间通信使用的数据结构:管道、socket、共享内存、消息队列、信号量等,是属于内核级的,一旦创建后就由内核管理,若进程不对其主动释放,那么这些变量会一直存在,除非重启系统。
不同进程是如何保证使用的内存互不影响?
多进程的执行通过内存保护实现互不干扰,如页式管理中有页地址越界保护,段式管理中有段地址越界保护。