进程
1:进程是指在系统中正在运行的一个应用程序,进程是程序在计算机上的一次执行活动。
2:每个进程之间是相互独立的, 每个进程均运行在其专用且受保护的内存空间内
(可以在 Mac 的活动指示器查看当前系统内运行的进程)
线程
1:一个进程要想执行任务,必须要有线程,至少有一条线程
2:一个线程的所有任务都是在线程中执行
线程的串行:一个线程中任务的执行是串行的, 也就是说同一时间内一个线程只能执行一个任务
进程和线程的比较
1:线程是 CPU 调用的最小单位
2:进程是 CPU 分配资源和调度的单位
3:iOS中的app都是单进程的,一个进程中可有多个线程,但至少要有一条线程
4:同一个进程内的线程共享进程资源
但是进程有前台进程和后台进程之分,用户看到的进程就是前台进程,当前台进程切换到后台时,会调用appdelegate的代理方法,但是进程本身还在,没有被kill掉
线程安全:要确保函数线程安全,主要需要考虑的是线程之间的共享变量。属于同一进程的不同线程会共享进程内存空间中的全局区和堆,而私有的线程空间则主要包括栈和寄存器。因此,对于同一进程的不同线程来说,每个线程的局部变量都是私有的,而全局变量、局部静态变量、分配于堆的变量都是共享的。在对这些共享变量进行访问时,如果要保证线程安全,则必须通过加锁的方式。一个函数被称为线程安全的,当且仅当被多个并发线程反复调用时,它会一直产生正确的结果。
要确保函数可重入,需满足一下几个条件:
1、不在函数内部使用静态或全局数据
2、不返回静态或全局数据,所有数据都由函数的调用者提供。
3、使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
4、不调用不可重入函数。
可重入与线程安全并不等同,一般说来,可重入的函数一定是线程安全的,但反过来不一定成立。
如果我们的线程函数不是线程安全的,那在多线程调用的情况下,可能导致的后果是显而易见的——共享变量的值由于不同线程的访问,可能发生不可预料的变化,进而导致程序的错误,甚至崩溃。
关于线程的堆栈
说一下线程自己的堆栈问题。
是的,生成子线程后,它会获取一部分该进程的堆栈空间,作为其名义上的独立的私有空间。(为何是名义上的呢?)由于,这些线程属于同一个进程,其他线程只要获取了你私有堆栈上某些数据的指针,其他线程便可以自由访问你的名义上的私有空间上的数据变量。(注:而多进程是不可以的,因为不同的进程,相同的虚拟地址,基本不可能映射到相同的物理地址)
多线程
一个进程可以开启多条线程,每条线程可以并行执行不同的任务. 这个技术可以提高程序的执行效率
多线程原理
同一时间,CPU 只能处理一条线程, 只有一条线程在工作
多线程并发执行,其实是 CPU 快速的在多条线程之间调度(切换)
如果 CPU 调度线程的时间足够快, 就造成了多线程并发执行的假象
说到并行计算,尤其是单台计算机的并行计算,一定要先建立时间片的概念。
我们现在所用的,不管是Windows还是Linux,一般都称为多任务操作系统,即,系统允许并行运行多个应用程序。
操作系统一般是按照一定策略,定期给每个活动的进程执行其内部程序的机会,并且每次只执行一小段时间,然后操作系统利用中断强行退出执行,将当前程序信息压栈,然后开始执行下一个进程的一小段程序。
通过这样不断快速的循环切换,每个程序都获得执行,在用户看来,感觉到很多程序都在平行的执行,这就模拟了并行计算。
当然,新的多核CPU以及超线程CPU,内部就有超过1个的CPU执行体,运行时就不是模拟了,而是真的有两个以上的程序在被执行。
当然在我们程序员看来,只需要理解程序是被操作系统片段执行的,每个片段就是一个时间片,就足够了。
既然是片段执行,程序员就必须理解,在自己的程序运行时不是独一无二的,我们看似很顺畅的工作,其实是由一个个的执行片段构成的,我们眼中相邻的两条语句甚至同一个语句中两个不同的运算符之间,都有可能插入其他线程或进程的动作。
由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。
时间片轮转调度
多线程的优点和缺点
优点
1:能适当的提高程序的执行效率
2:能适当提高资源利用率(CPU 内存利用率)
缺点
创建多线程是有开销的,iOS 下主要成本包括: 内核数据结构,(大约1KB), 栈空间(子线程512KB, 主线程1MB, 也可以使用 -setStackSize: 设置, 但必须是4K 的倍数, 而且最小是16K), 创建线程大约需要90毫秒的创建时间
如果开启大量线程, 会降低程序的性能
线程越多,CPU 在调度线程上的开销就越大
程序设计更加复杂: 比如线程之间的通信, 多线程的数据共享
多线程在 iOS 开发中的应用
主线程
一个 iOS 程序运行后, 默认会开启一条线程, 成为"主线程"或者"UI 线程"
主线程的主要作用
1:显示/刷新 UI 界面
2:处理 UI 事件(比如点击事件, 滚动事件, 拖拽事件)
主线程的注意点
1:不要将比较耗时的操作放到主线程
2:耗时操作会卡住主线程, 严重影响 UI 的流畅度, 给用户一种 "卡"的坏体检
iOS 中多线程的实现方案
技术方案 简介 语言 线程生命周期 使用频率
NSThread 使用更加面向对象
简单易用, 可直接操作线程对象 OC 程序员管理 偶尔使用
GCD 旨在替代 NSThread 的线程技术
充分利用设备的多核 C 自动管理 经常使用
NSOperation 基于 GCD(底层是GCD)
比GCD 多了一些更简单实用的功能
使用更加面向对象 OC 自动管理 经常使用
线程是指进程内的一个执行单元,也是进程内的可调度实体
与进程的区别:
(1)地址空间:进程内的一个可执行单元,进程至少有一个线程;他们共享进程的地址空间(也有少量自己的地址空间);而进程有自己独立的地址空间(多个进程之间一般不会共享地址空间)
(2)资源拥有:进程是资源拥有的单位,同一个进程内的线程共享进程的资源
(3)线程是处理器调度和分派的基本单位
(4)二者均可并发执行,多线程程序的并发性高
(5)进程的切换代价高于线程,多线程的并发性高
进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其他进程产生影响,而线程只是第一个进程中的不同执行路径,线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些.但对于一些要求同时进行并且又要共享某些变量的并发操作,需要用多线程
2.Unix和windows进程间通信的主要方式
linux系统IPC:
管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
命名管道(named pipe): 命名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
信号量( semophore ): 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
消息队列( message queue ): 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
信号( sinal ):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
共享内存( shared memory ):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
套接字( socket ): 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
windows系统IPC:
剪贴板(Clipboard):当用户在应用程序中执行剪切或复制操作时,应用程序将选定的数据以一个或多个标准或应用程序定义的格式放在剪贴板中。
WM_COPYDATA消息:当一个应用向另一个应用传送数据时,发送方只需使用调用SendMessage函数,接收方只需像处理其它消息那样处理WM_COPYDATA消息,这样收发双方就实现了数据共享,它在底层实际上是通过文件映射来实现的。
文件映射(File Mapping ):使进程把文件内容当作进程地址区间一块内存那样来对待。只需简单的指针操作就可读取和修改文件的内容。允许多个进程访问同一文件映射对象,各个进程在它自己的地址空间里接收内存的指针,通过使用这些指针,不同进程就可以读写文件的内容,实现了对文件中数据的共享。
共享内存(Shared Memory)是文件映射的一种特殊情况进程在创建文件映射对象时用0xFFFFFFFF来代替文件句柄(HANDLE),就表示了对应的文件映射对象是从操作系统页面文件访问内存,其它进程打开该文件映射对象就可以访问该内存块。由于共享内存是用文件映射实现的,所以它也有较好的安全性,也只能运行于同一计算机上的进程之间。
动态数据交换(DDE):是使用共享内存在应用程序之间进行数据交换的一种进程间通信形式。应用程序可以使用DDE进行一次性数据传输,也可以当出现新数据时,通过发送更新值在应用程序间动态交换数据。DDE和剪贴板一样既支持标准数据格式(如文本、位图等),又可以支持自己定义的数据格式。但它们的数据传输机制却不同,一个明显区别是剪贴板操作几乎总是用作对用户指定操作的一次性应答,如从菜单中选择Paste命令。尽管DDE也可以由用户启动,但它继续发挥作用一般不必用户进一步干预。可以发生在单机或网络中不同计算机的应用程序之间。
邮件槽(Mailslot):提供进程间单向通信能力,任何进程都能建立邮件槽成为邮件槽服务器。其它进程称为邮件槽客户,可以通过邮件槽的名字给邮件槽服务器进程发送消息。进来的消息一直放在邮件槽中,直到服务器进程读取它为止。一个进程既可以是邮件槽服务器也可以是邮件槽客户,因此可建立多个邮件槽实现进程间的双向通信。
管道( pipe ):同上linux系统&命名管道
套接字(Sockets):同上linux系统
3.死锁
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的僵局,若无外力作用,它们都将无法推进下去。
产生死锁的四个必要条件:
1.互斥条件:一段时间内某资源只由一个进程占有。
2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3.不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
预防死锁:需要打破必要条件的2,3,4中之一,由于施加的限制条件较严格,可能导致系统资源利用率和系统吞吐量降低。
避免死锁:施加的限制条件较弱,使系统一直处于安全状态。比如银行家算法。
检测死锁:资源分配图、死锁定理。
解除死锁:剥夺起源、撤销进程。
4.linux下fork函数
在fork()的调用处,创建一个子进程,并将整个父进程空间会原模原样地复制到子进程中,包括指令,变量值,程序调用栈,环境变量,缓冲区等。fork调用仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
(1)在父进程中,fork返回新创建子进程的进程ID;
(2)在子进程中,fork返回0;
(3)如果出现错误,fork返回一个负值;
在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
fork出错可能有两种原因:
1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
2)系统内存不足,这时errno的值被设置为ENOMEM。
创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
对于这种N次循环的情况,执行printf函数的次数为
2*(2^N-1)次,创建的子进程数为2^N-1个。输出中没有换行时缓冲区也会被复制
6.程序什么时候使用多线程好,什么时候单线程效率高
1.耗时的操作使用线程,提高应用程序响应速度
2.并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求
3.多CPU系统中,使用线程提高CPU利用率
4.改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。其他情况都使用单线程。
7.线程间通信
互锁函数、临界段、内核对象(事件对象、互斥对象、信号量)
8.进程状态转换
在操作系统中,进程一般有三种基本状态:运行状态,就绪状态和等待状态。
1)就绪——执行:对就绪状态的进程,当进程调度程序按一种选定的策略从中选中一个就绪进程,为之分配了处理机后,该进程便由就绪状态变为执行状态;
2)执行——等待:正在执行的进程因发生某等待事件而无法执行,如进程提出输入/输出请求而变成等待外部设备传输信息的状态,进程申请资源(主存空间或外部设备)得不到满足时变成等待资源状态,进程运行中出现了故障(程序出错或主存储器读写错等)变成等待干预状态等等;
3)等待——就绪:处于等待状态的进程,在其等待的事件已经发生,如输入/输出完成,资源得到满足或错误处理完毕时,处于等待状态的进程并不马上转入执行状态,而是先转入就绪状态,然后再由系统进程调度程序在适当的时候将该进程转为执行状态;
4)执行——就绪:正在执行的进程,因时间片用完而被暂停执行,或在采用抢先式优先级调度算法的系统中,当有更高优先级的进程要运行而被迫让出处理机时,该进程便由执行状态转变为就绪状态。
9.内存地址:虚拟地址-线性地址-物理地址的区别与联系
x86平台下的系统采用分段机制与分页机制对地址进行转换,其中分段机制把一个虚拟地址转换成线性地址;分页机制把一个线性地址转换成物理地址