本章内容
- 操作系统发展史介绍
- 进程与线程的区别
- python GIL全局解释器锁
- 线程
- 语法
- join
- 线程锁LockRlock信号量
- 将线程变为守护进程
- Event时间
- Queue队列
- 生产者消费者模型
- 进程
- 语法
- 进程间通讯
- 进程池
一、操作系统发展史介绍
手工操作(无操作系统)
1946年第一台计算机诞生--20世纪50年代中期,还未出现操作系统,计算机工作采用手工操作方式。
手工操作
程序员将对应于程序和数据的已穿孔的纸带(或卡片)装入输入机,然后启动输入机把程序和数据输入计算机内存,接着通过控制台开关启动程序针对数据运行;计算完毕,打印机输出计算结果;用户取走结果并卸下纸带(或卡片)后,才让下一个用户上级。
手工操作方式两个特点:
(1)用户独占全机。不会出现因资源已被其他用户占用而等待的现象,但资源的利用率低。
(2)CPU等待手工操作。CPU利用不充分。
20世纪50年代后期,出现人机矛盾:手工操作的慢速度和计算机的高速度之间形成了尖锐矛盾,手工操作方式已严重损害了系统资源的利用率(使资源利用率降为百分之几,甚至更低),不能容忍。唯一的解决办法:只有摆脱人的手工操作,实现作业的自动过渡。这样就出现了成批处理。
批处理系统
批处理系统:加载在计算机上的一个系统软件,在它的控制下,计算机能够自动的、成批的处理一个或多个用户的作业(这作业包括程序、数据和命令)。
联机批处理系统
首先出现的是联机处理系统,即作业的输入/输出由CPU来处理。
助记与输入机之间增加一个存储设备--磁带,在运行于主机上的监督程序的自动控制下,计算机可自动完成:成批的把输入机上的用户作业读入磁带,依次把磁带上的用户作业读入助记内存并执行并把计算结果向输出机输出。完成了上一批作业后,监督程序又从输入机上输入另一批作业,保存在磁带上,并按上述步骤重复处理。
监督程序不停的处理各个作业,从而实现了作业到作业的自动转接,减少了作业建立时间和手工操作时间,有效克服了人机矛盾,提高了计算机的利用率。
但是,在作业输入和结果输出时,主机的高速CPU仍处于空闲状态,等待慢速的输入/输出设备完成工作:主机处于“忙等”状态。
脱机批处理系统
为克服与换届告诉主机与慢速外设的矛盾,提高CPU的利用率,又引入了脱机批处理系统,即输入/输出脱离主机控制。
这种方式的显著特征是:增加一台不与主机直接相连而专门用于输入/输出设备打交道的卫星机。
其功能是:
(1)从输入机上读取用户作业并放到输入磁带上。
(2)从输出磁带上读取执行结果并传给输出机。
这样,主机不是直接与慢速的输入/输出设备打交道,而是与速度相对较快的磁带机发生关机,有效的缓解了主机与设备的矛盾。主机与卫星机可并行工作,二者分工明细,可以充分发挥主机的高速计算能力。
脱机批处理系统20世纪60年代应用十分广泛,它极大缓解了人机矛盾及主机与外设的矛盾。IBM-7090/7094:配备的监督程序就是脱机批处理系统,是现代操作系统的原型。
不足:每次主机内存中仅存放一道作业,每当它运行期间发出输入/输出(I/O)请求后,高速的CPU便处于等待低速的I/O完成状态,致使CPU空闲。
为改善CPU的利用率,又引入了多道程序系统。
多道程序系统
多道程序设计技术
所谓多道程序设计技术,就是指允许多个程序同时进入内存并运行。即同时把多个程序放入内存,并允许他们交替在CPU中运行,它们共享系统中的各种硬、软件资源。当一道程序因I/O请求而暂停运行时,CPU便立即转去运行另一道程序。
单道程序的运行过程:
在A程序计算时,I/O空闲,A程序I/O操作时,CPU空闲(B程序也是同样);必须A工作完成后,B才能进入内存中开始工作,两者是串行的,全部完成共需时间= T1+T2.
多道程序的运行过程:
将A、B两道程序同时存放在内存中,他们在系统的控制下,可相互穿插、交替的在CPU上运行:当A程序因请求I/O设备也不空闲,显然,CPU和I/O设备都处于“忙”状态,大大提高了资源的利用率,从而也提高了系统的效率,A、B全部完成所需时间<<T1+T2。
多道程序设计技术不仅使CPU得到充分利用,同时改善I/O设备和内存的利用率,从而提高了整个系统的资源利用率和系统吞吐量(单位时间内处理作业(程序)的个数),最终提高了整个系统的效率。
单处理机系统中多道程序运行时的特点:
(1)多道:计算机内存中同时存放几道相互独立的程序;
(2)宏观上并行:同时进入系统的几道程序都处于运行过程中,即它们先后开始了各自的运行,但都未运行完毕;
(3)微观上串行:实际上,各道程序轮流地用CPU,并交替运行。
多道程序系统的出现,标志着操作系统渐趋成熟的阶段,先后出现了作业调度管理、处理机管理、存储器管理、外部设备管理、文件系统管理等功能。
多道批处理系统
20世纪60年代中期,在前述的批处理系统中,引入多道程序设计技术后形成多道批处理系统(简称:批处理系统)。
它有两个特点:
(1)多道:系统内可同时容纳多个作业。这些作业放在外存中,组成一个后备队列,系统按一定的调度原则每次从后备作业队列中选取一个或多个作业进入内存运行,运行作业结束、退出运行和后备作业进入运行均由系统自动实现,从而在系统中形成一个自动转接的、连续的作业流。
(2)成批:在系统运行过程中,不允许用户与其作业发生交互作用,即:作业一旦进入系统,用户就不能直接干预其作业的运行。
批处理系统的追求目标:提高系统资源利用率和系统吞吐量,以及作业流程的自动化。
批处理系统的一个重要缺点:不提供人机交互能力,给用户使用计算机带来不便。
虽然用户独占全机资源,并且直接控制程序的运行,可以随时了解程序运行情况。但这种工作方式因独占全机造成资源效率极低。
一种新的追求目标:既能保证计算机效率,又能方便用户使用计算机。 20世纪60年代中期,计算机技术和软件技术的发展使这种追求成为可能。
分时系统
由于CPU速度不断提高和采用分时技术,一台计算机可同时连接多个用户终端,而每个用户可在自己的终端上联机使用计算机,好象自己独占机器一样。
分时技术:把处理机的运行时间分成很短的时间片,按时间片轮流把处理机分配给各联机作业使用。
若某个作业在分配给它的时间片内不能完成其计算,则该作业暂时中断,把处理机让给另一作业使用,等待下一轮时再继续其运行。由于计算机速度很快,作业运行轮转得很快,给每个用户的印象是,好象他独占了一台计算机。而每个用户可以通过自己的终端向系统发出各种操作控制命令,在充分的人机交互情况下,完成作业的运行。
具有上述特征的计算机系统称为分时系统,它允许多个用户同时联机使用计算机。
特点:
(1)多路性。若干个用户同时使用一台计算机。微观上看是各用户轮流使用计算机;宏观上看是各用户并行工作。
(2)交互性。用户可根据系统对请求的响应结果,进一步向系统提出新的请求。这种能使用户与系统进行人机对话的工作方式,明显地有别于批处理系统,因而,分时系统又被称为交互式系统。
(3)独立性。用户之间可以相互独立操作,互不干扰。系统保证各用户程序运行的完整性,不会发生相互混淆或破坏现象。
(4)及时性。系统可对用户的输入及时作出响应。分时系统性能的主要指标之一是响应时间,它是指:从终端发出命令到系统予以应答所需的时间。
分时系统的主要目标:对用户响应的及时性,即不至于用户等待每一个命令的处理时间过长。
分时系统可以同时接纳数十个甚至上百个用户,由于内存空间有限,往往采用对换(又称交换)方式的存储方法。即将未“轮到”的作业放入磁盘,一旦“轮到”,再将其调入内存;而时间片用完后,又将作业存回磁盘(俗称“滚进”、“滚出“法),使同一存储区域轮流为多个用户服务。
多用户分时系统是当今计算机操作系统中最普遍使用的一类操作系统。
实时系统
虽然多道批处理系统和分时系统能获得较令人满意的资源利用率和系统响应时间,但却不能满足实时控制与实时信息处理两个应用领域的需求。于是就产生了实时系统,即系统能够及时响应随机发生的外部事件,并在严格的时间范围内完成对该事件的处理。
实时系统在一个特定的应用中常作为一种控制设备来使用。
实时系统可分成两类:
(1)实时控制系统。当用于飞机飞行、导弹发射等的自动控制时,要求计算机能尽快处理测量系统测得的数据,及时地对飞机或导弹进行控制,或将有关信息通过显示终端提供给决策人员。当用于轧钢、石化等工业生产过程控制时,也要求计算机能及时处理由各类传感器送来的数据,然后控制相应的执行机构。
(2)实时信息处理系统。当用于预定飞机票、查询有关航班、航线、票价等事宜时,或当用于银行系统、情报检索系统时,都要求计算机能对终端设备发来的服务请求及时予以正确的回答。此类对响应及时性的要求稍弱于第一类。
实时操作系统的主要特点:
(1)及时响应。每一个信息接收、分析处理和发送的过程必须在严格的时间限制内完成。
(2)高可靠性。需采取冗余措施,双机系统前后台工作,也包括必要的保密措施等。
操作系统发展图谱
二、进程与线程的区别
程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称为进程。程序和进程的区别在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。
在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。
有了进程为什么还要线程?
进程有很多优点,它提供了多道编程,让我们感觉每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解,既然进程这么优秀,为什么还要线程呢?因为进程还是有很多缺陷的,主要体现在两点上:
- 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
- 进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
例如:
我们在使用qq聊天,qq作为一个独立进程如果同一时间只能干一件事,那他如果实现在同一时刻即能监听键盘输入、又能监听其他人给你发的消息、同时还能把别人发的消息显示在屏幕上呢?你会说,操作系统不是有分时么?但是,分时是指在不同进程间的分时,即操作系统处理一会你的qq任务,又切换到word文档任务上了,每个CPU时间片分给你的qq程序时,你的qq还是只能同时干一件事。比如一个操作系统就像一个工厂,工厂里面有很多个生产车间,不同的车间生产不同的产品,每个车间就相当于一个进程,且你的工厂又穷,供电不足,同一时间只能给一个车间供电,为了能让所有车间同时生产,你的工厂的电工只能给不同的车间分时供电,但是轮到你的qq车间时,发现只有一个干活的工人,结果生产效率极地,为了解决这个问题,你的办法就是多加几个工人,让几个工人并行工作,这每个工人就是一个线程!
线程:是操作系统能够进行运算调度最小的单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
所以线程与进程的区别如下:
-
-
- 线程共享创建它的进程的地址空间,进程有自己的地址空间。
- 线程可以直接访问进程的数据段;进程有自己的父进程的数据段的副本。
- 线程可以直接与其他线程进行通信,但是必须通过进程来实现,并且是同一进程里的线程。
- 新线程容易创建,新进程需要重复父进程。
- 线程可以对同一进程的线程进行控制,进程只能对子进程进行控制。
- 主进程(取消、优先级更改等)的更改可能会影响进程的其他线程的行为;父进程的更改不会影响子进程。
-
三、python GIL全局解释器锁
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
上面的核心意思是,无论你启动多少个线程,你有多少个CPU,Python在执行的时候会淡定的在同一时刻只允许一个线程运行。首先我们要明确的是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不容的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIl归结为Python语言的缺陷。所以要明确:GIL并不是Python的特性,Python完全可以不依赖于GIL
强烈推荐-->GIL对Pthon多线程的影响 (妈的,我看不懂英文,给我等着等我学好英文,到是要看看)
四、线程
Python threading模块
线程有2中调用方式,如下:
直接调用
1 #!/usr/bin/env python
2 # -*- coding:utf-8 -*-
3 # Author:Lyon
4
5 import threading,time
6
7 def sayhi(num): #定义每个线程要运行的函数
8
9 print("running on number:%s"% num)
10
11 time.sleep(2)
12
13 if __name__ == '__main__':
14 #生成一个线程实例
15 t1 = threading.Thread(target=sayhi,args=(1,))
16 #生成第二个线程实例
17 t2 = threading.Thread(target=sayhi,args=(2,))
18 #启动线程t1
19 t1.start()
20 #启动线程t2
21 t2.start()
22 #获取线程名
23 print(t1.getName())
24 #获取线程名
25 print(t2.getName())
继承式调用
1 #!/usr/bin/env python
2 # -*- coding:utf-8 -*-
3 # Author:Lyon
4
5 import threading,time
6
7 class LyonThread(threading.Thread):
8 def __init__(self,num):
9 threading.Thread.__init__(self)
10 self.num = num
11
12 #定义每个线程要运行的函数
13 def run(self):
14 print("running on number:%s"%self.num)
15 time.sleep(2)
16
17
18 if __name__ == '__main__':
19
20 t1 = LyonThread(1)
21 t2 = LyonThread(2)
22 t1.start()
23 t2.start()
Join&Daemon
很多线程做后台任务,如发送实时数据爆,或进行定期的垃圾清理,或什么的。这些只有在主程序运行时才有用,一旦其他守护进程、线程退出,就可以杀死它们。如果没有守护线程,你必须跟踪他们,并告诉他们退出,你的程序才可以完全退出。
1 #!/usr/bin/env python
2 # -*- coding:utf-8 -*-
3 # Author:Lyon
4
5 import time,threading
6
7 def run(n):
8 print("%s -------running----
"%n)
9 time.sleep(0)
10 print("--done--")
11
12 def main():
13 for i in range(5):
14 #生成线程实例
15 t = threading.Thread(target=run,args=[i,])
16 #启动线程
17 t.start()
18 #join的作用就是等待所有线程执行完成最后再执行主线程,保证所有线程执行完毕
19 t.join()
20 print("starting thread",t.getName())
21
22
23 m = threading.Thread(target=main,args=[])
24 #将main线程设置为Daemon线程,它作为程序主线程的守护线程,当主线程退出时,
25 #m线程也会退出,由m启动的其它子线程会同时退出,不管是否执行完任务
26 m.setDaemon(True)
27 m.start()
28 m.join(timeout=2)
29 print("---main thread done----")
线程锁(互斥锁Mutex)
一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据,会出现什么状况?
1 #!/usr/bin/env python
2 # -*- coding:utf-8 -*-
3 # Author:Lyon
4
5 import time,threading
6
7 def addNum():
8 global num #在每个线程中获取这个全局变量
9 print("--get num:",num)
10 time.sleep(1)
11 num -=1#对此公共变量进行-1操作
12
13 num = 100 #设定一个共享变量
14 thread_list = []
15 for i in range(100):
16 t = threading.Thread(target=addNum)
17 t.start()
18 thread_list.append(t)
19
20 for t in thread_list:#等待所有线程执行完毕
21 t.join()
22
23 print("final num:",num)
正常来讲,这个num结果应该是0,但在python2.7上多运行几次,会发现,最后打印出来的num结果不总是0,为什么每次运行的结果不一样呢?很简单,假设你有A,B两个线程,此时都要对num进行减1操作,由于2个线程是并发同时运行的,所以2个线程很有可能同时拿走了num=100这个初始变量交给CPU去运算,当A线程去处理完的结果是99,但此时B线程运算完的结果也是99,两个线程同时CPU运算的结果再赋值给num变量后,结果就都是99。每个线程在要修改公共数据时,为了便面自己在还没有改完的时候别人也来修改此数据,可以给这个数据加一把锁,这样其他线程想修改此数据时就必须等待你修改完毕并把锁释放掉后才能再访问此数据。
注:不要在3.x上运行,不知道为什么,3.x上的结果总是正确的,可能是自动加了锁
加锁版本
1 #!/usr/bin/env python
2 # -*- coding:utf-8 -*-
3 # Author:Lyon
4 import time,threading
5 def addNum():
6
7 #在每个线程中都获取这个人全局变量
8 time.sleep(1)
9 lock.acquire()#修改数据前加锁
10 global num
11 num -=1#对此公共变量进行-1操作
12 lock.release()#修改后释放
13 print("get num",num)
14
15 num = 100#设定一个共享变量
16 thread_list = []
17 lock =threading.Lock()#生成全局锁
18 for i in range(100):
19 t = threading.Thread(target=addNum)
20 t.start()
21 thread_list.append(t)
22 for t in thread_list:#等待所有线程执行完毕
23 t.join()
24 print("final num:",num)
GIL VS Lock
上面我们知道python中已经有一个GIL来保证同一时间只能有一个线程来执行,为什么还需要Lock?因为这里的lock是用户级的lock,跟那个GIl没关系,看下图:
Python中GIL主要的原因是为了降低程序的开发的复杂度,比如现在你写python不需要关心内存回收的问题,因为Python解释器帮你自动定期进行内存回收,你可以理解为Python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看那些内存数据是可以被清空的,此时你自己的程序里的线程和py解释器自己的线程是并发运行的,假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其他线程正好又重新给这个还没来得及清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其他人都不能动,这样就解决了上述的问题,这可以说是Python早期版本的遗留问题。
RLock(递归锁)
其实就是大锁中再包含一个子锁
1 #!/usr/bin/env python
2 # -*- coding:utf-8 -*-
3 # Author:Lyon
4
5 import threading,time
6
7 def run1():
8 print("grab the first part data")
9 #加锁
10 lock.acquire()
11 global num
12 num +=1
13 #释放锁
14 print("i am num:",num)
15 lock.release()
16 return num
17 def run2():
18 print("grab the second part data")
19 lock.acquire()
20 global num2
21 num2 +=1
22 lock.release()
23 print("i am num2",num2)
24 return num2
25 def run3():
26 lock.acquire()
27 res = run1()
28 print("------between run1 and run2------")
29 res2 = run2()
30 lock.release()
31 print(res,res2)
32
33 if __name__ =='__main__':
34 num,num2 = 0,0
35 lock=threading.RLock() #作用是在一个线程里面可以上多个锁, 而threading.Lock则只能加一个
36 for i in range(10):
37 t = threading.Thread(target=run3)
38 t.start()
39 while threading.active_count()!=1:
40 print(threading.active_count())#打印当前活动的线程个数
41 else:
42 print("-----all threads done----")
43 print(num,num2)
Semaphore(信号量)
互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。
1 #!/usr/bin/env python
2 # -*- coding:utf-8 -*-
3 # Author:Lyon
4
5 import threading,time
6
7 def run(n):
8 semaphore.acquire()
9 time.sleep(1)
10 print("run thr thread:%s
"%n)
11 semaphore.release()
12
13 if __name__=='__main__':
14 num = 0
15 #最多允许五个线程同时运行
16 semaphore = threading.BoundedSemaphore(5)
17 for i in range(20):
18 t = threading.Thread(target=run,args=(i,))
19 t.start()
20
21 while threading.active_count() !=1:
22 pass #print threading.active_count()
23 else:
24 print("-----all threads done-----")
25 print(num)
Timer
这个类代表一个动作,只有在异地昂数量的时间通过后才能运行定时器开始计时,如线程,调用start()方法。定时器可以停止(之前的行动已经开始,通过调用thecancel()方法)。计时器在执行其动作之前会等待的间隔可能与用户指定的时间间隔不完全相同。
1 #!/usr/bin/env python
2 # -*- coding:utf-8 -*-
3 # Author:Lyon
4
5 def hello():
6 print("hello world")
7
8 t = Timer(2.0,hello)
9 #after 2 seconds, "hello world" will be printed
10 t.start()
Events
事件是一个简单的同步对象;
该事件表示内部标志和线程
可以等待设置标志,或设置或清楚标志本身。
事件 = event()线程。
#客户端线程可以等待标志被设置
wait()事件。
#服务器线程可以设置或重置
set()事件。
clear()事件。
如果设置了标志,则等待方法不做任何事情。
如果清除了标志,等待将被阻止,直到它重新设置。
任意数量的线程都可以等待相同的事件。
通过Event来实现两个或多个线程间的交互,下面是一个i额红绿灯的例子,即启动一个线程做交通指挥灯,生成几个线程做车辆,车辆行驶按红灯停,绿灯行的规则。
1 #!/usr/bin/env python
2 # -*- coding:utf-8 -*-
3 # Author:Lyon
4
5 import time
6 import threading
7 event = threading.Event()
8 def lighter():
9 count = 0
10 #设置绿灯
11 event.set()
12 while True:
13 #改成红灯
14 if count>5 and count<10:
15 #把标志位清除
16 event.clear()
17 print("