并发编程-进程、线程、协程
为什么要有操作系统?
操作系统是一个用来协调、管理和控制计算机硬件和软件资源的系统程序,它位于硬件和应用程序之间。 (程序是运行在系统上的具有某种功能的软件,比如说浏览器,音乐播放器等。) 操作系统的内核的定义:操作系统的内核是一个管理和控制程序,负责管理计算机的所有物理资源,其中包括:文件系 统、内存管理、设备管理和进程管理。
什么是进程(process)?
进程(process),是计算机中已运行程序的实体,是线程的容器,是最小的资源单位;一个进程至少有一个线程
进程包括三个部分:程序、数据集、进程控制块
假如有两个程序A和B,程序A在执行到一半的过程中,需要读取大量的数据输入(I/O操作),而此时CPU只能静静地等待任务A读取完数据才能继续执行,这样就白白浪费了CPU资源。是不是在程序A读取数据的过程中,让程序B去执行,当程序A读取完数据之后,让
程序B暂停,然后让程序A继续执行?当然没问题,但这里有一个关键词:切换既然是切换,那么这就涉及到了状态的保存,状态的恢复,加上程序A与程序B所需要的系统资源(内存,硬盘,键盘等等)是不一样的。自然而然的就需要有一个东西去记录程序A和程序B
分别需要什么资源,怎样去识别程序A和程序B等等,所以就有了一个叫进程的抽象进程定义:
进程就是一个程序在一个数据集上的一次动态执行过程。进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系
统感知进程存在的唯一标志。
举一例说明进程:
想象一位有一手好厨艺的计算机科学家正在为他的女儿烘制生日蛋糕。他有做生日蛋糕的食谱,厨房里有所需的原料:面粉、鸡蛋、糖、香草汁等。在这个比喻中,做蛋糕的食谱就是程序(即用适当形式描述的算法)计算机科学家就是处理器(cpu),
而做蛋糕的各种原料就是输入数据。进程就是厨师阅读食谱、取来各种原料以及烘制蛋糕等一系列动作的总和。现在假设计算机科学家的儿子哭着跑了进来,说他的头被一只蜜蜂蛰了。计算机科学家就记录下他照着食谱做到哪儿了(保存进程的当前状态),然后拿出一本急救手册,按照其中的指示处理蛰伤。这里,
我们看到处理机从一个进程(做蛋糕)切换到另一个高优先级的进程(实施医疗救治),每个进程拥有各自的程序(食谱和急救手册)。当蜜蜂蛰伤处理完之后,这位计算机科学家又回来做蛋糕,从他离开时的那一步继续做下去。
什么是线程?
线程是操作系统能够进行运算调度的最小单位。线程被包含在进程之中(进程相当于一个容器),是进程中的实际运作单位。
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一样事的缺陷,使到进程内并发成为可能。 假设,一个文本程序,需要接受键盘输入,将内容显示在屏幕上,还需要保存信息到硬盘中。若只有一个进程,势必造成同一时间只能干一样事的尴尬(当保存时,就不能通过键盘输入内容)。若有多个进程,每个进程负责一个任务,进程A负责接收键盘输入的任务,进程B负责将内容显示在屏幕上的任务, 进程C负责保存内容到硬盘中的任务。这里进程A,B,C间的协作涉及到了进程通信问题,而且有共同都需要拥有的东西-------文本内容,不停的切换造成性能上的损失。若有一种机制,可以使 任务A,B,C共享资源,这样上下文切换所需要保存和恢复的内容就少了,同时又可以减少通信所带来的性能损耗,那就好了。是的,这种机制就是线程。 线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序 计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发 性能。线程没有自己的系统资源。
进程与线程的关系和比较
1、一个程序至少有一个进程,一个进程至少有一个线程。(进程可以理解成线程的容器) 2、进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。 3、线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。 4、进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)但是它可与同属一个进程的其他的线程共享所在的进程拥有的全部资源. 一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。
threading模块
一、 线程的两种调用方式
threading 模块建立在thread 模块之上。thread模块以低级、原始的方式来处理和控制线程,而threading 模块通过对thread进行二次封装,提供了更方便的api来处理线程。
直接调用:
import threading import time def sayhi(num): #定义每个线程要运行的函数 print("running on number:%s" %num) time.sleep(3) if __name__ == '__main__': t1 = threading.Thread(target=sayhi,args=(1,)) #生成一个线程实例 t2 = threading.Thread(target=sayhi,args=(2,)) #生成另一个线程实例 t1.start() #启动线程 t2.start() #启动另一个线程 print(t1.getName()) #获取线程名 print(t2.getName())
继承式调用:
'''继承式调用(必须重写run()方法)''' import threading from time import ctime,sleep class MyThread(threading.Thread): def __init__(self,num,tm): threading.Thread.__init__(self) self.num = num self.tm = tm def run(self): print("running on number:%s" % self.num,ctime()) sleep(self.tm) print("end running the number",ctime()) threads = [] t1 = MyThread(11,3) t2 = MyThread(22,5) threads.append(t1) threads.append(t2) if __name__ == '__main__': t2.setDaemon(True) for t in threads: t.start() # for t in threads: # t.join() t1.join() print("end the main threading!")
二 、t1 = threading.Thread() 实例对象方法
t1.join() --->加入主线程 , t1.setDaemon() --->守护主线程(与主线程同时退出)
import threading from time import ctime,sleep def ListenMusic(name): print ("Begin listening to %s. %s" %(name,ctime())) sleep(3) print("end listening %s"%ctime()) def RecordBlog(title): print ("Begin recording the %s! %s" %(title,ctime())) sleep(5) print('end recording %s'%ctime()) t1 = threading.Thread(target=ListenMusic,args=("小苹果",)) t2 = threading.Thread(target=RecordBlog,args=("cnblog",)) threads = [] threads.append(t1) threads.append(t2) if __name__ == '__main__': t2.setDaemon(True) for t in threads: t.start() # for t in threads: # t.join() t1.join() print("the mian thread has done!",ctime())
join():在子线程完成运行之前,主线程将一直被阻塞
setDaemon(True):将线程声明为守护线程,必须在 t1.start() 方法调用之前设置, 如果不设置为守护线程程序会被无限挂起。这个方法基本和join是相反的。
join() 方法:当我们 在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程 就分兵两路,分别运行。那么当主线程完成想退出时,会检验子线程是否完成。如果子线程未完成,则主线程会等待子线程完成后再退出。
setDaemon() :只要主线程完成了,不管子线程是否完成,都要和主线程一起退出。
ListenMusic 等待3s
RecordBlog 等待5s
当t2.setDaemon(True)时,t2 为守护进程,t1.join() t1未完成时 主线程阻塞
其它方法:
threading.Thread() 实例化对象的方法
# run(): 线程被cpu调度后自动执行线程对象的run方法 # start():启动线程活动。 # isAlive(): 返回线程是否活动的。 # getName(): 返回线程名。 # setName(): 设置线程名。 threading 类模块提供的一些方法: # threading.currentThread() : 返回当前的线程变量。 # threading.enumerate() : 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。 # threading.activeCount() : 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
概念辨析:并行&并发
并发:是指系统具有处理多个任务(动作)的能力(注:cpu处理速度非常快 切换处理多个任务 人为感知不到)
并行:是指系统具有 同时 处理多个任务(动作)的能力
并行是并发的一个子集
概念辨析:同步&异步
当进程执行到一个I/O(等待外部数据)的时候 ---->等:同步
---->不等:异步(一直等到数据接收完成,再回来处理)
异步的效率较高
三、Python的GIL (Global Interpreter Lock 全局解释器锁)
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在执行的时候会淡定的在同一时刻只允许一个线程运行
任务:IO密集型(工作中大多数遇到的情况都是IO密集型的任务)
计算密集型
结论:
对于IO密集型任务:Python的多线程是有意义的,可以采用多线程+协程
对于计算密集型的任务:Python的多线程就不推荐了,Python语言就不适用了。
四、 同步锁(Lock)
import time import threading def addNum(): global num #在每个线程中都获取这个全局变量 #num-=1 temp=num time.sleep(0.01) num =temp-1 #对此公共变量进行-1操作 num = 100 #设定一个共享变量 #生成100个线程 thread_list = [] for i in range(100): t = threading.Thread(target=addNum) t.start() thread_list.append(t) #主线程等待所有子线程执行完毕 for t in thread_list: t.join() print('final num:', num )
观察:time.sleep(0.1) /0.001/0.0000001 结果分别是多少?
多个线程都在同时操作同一个共享资源,所以造成了资源破坏,怎么办呢?
我们可以通过同步锁来解决这种问题,锁之间的内容被锁定,不允许CPU切换,被锁包住的部分是串行操作,所有编程语言在这里都必须这么做。
R=threading.Lock() #### def sub(): global num R.acquire() temp=num-1 time.sleep(0.1) num=temp R.release()
五、线程死锁和递归锁
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。因为系统判断这部分资源都正在使用,所有这两个线程在无外力作用下将一直等待下去。下面是一个死锁的例子:
'''死锁''' import time import threading class MyThread(threading.Thread): def actionA(self): lockA.acquire() print(self.name,'actionA-gotA',time.ctime()) time.sleep(2) lockB.acquire() print(self.name,'actionA-gotB',time.ctime()) time.sleep(1) lockB.release() lockA.release() def actionB(self): lockB.acquire() print(self.name, 'actionB-gotB', time.ctime()) time.sleep(2) lockA.acquire() print(self.name, 'actionB-gotA', time.ctime()) time.sleep(1) lockA.release() lockB.release() def run(self): self.actionA() self.actionB() if __name__ == '__main__': lockA = threading.Lock() lockB = threading.Lock() thread_list = [] for i in range(5): t = MyThread() #创建5个线程 t.start() thread_list.append(t) for t in thread_list: t.join() print("ending the main thread!")
解决办法:使用递归锁
lockA=threading.Lock() lockB=threading.Lock() #--------------r_lock=threading.RLock()
为了支持在同一线程中多次请求同一资源,python提供了“可重入锁”:threading.RLock。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。
直到一个线程所有的acquire都被release,其他的线程才能获得资源。
'''解决死锁办法 ---> 递归锁''' import time import threading class MyThread(threading.Thread): def actionA(self): # lockA.acquire() r_lock.acquire() #锁数量count=1 print(self.name,'actionA-gotA',time.ctime()) time.sleep(2) # lockB.acquire() r_lock.acquire() #count=2 print(self.name,'actionA-gotB',time.ctime()) time.sleep(1) # lockB.release() # lockA.release() r_lock.release() #锁数量count=1 r_lock.release() #锁数量count=0 def actionB(self): # lockB.acquire() r_lock.acquire() #锁数量count=1 print(self.name, 'actionB-gotB', time.ctime()) time.sleep(2) # lockA.acquire() r_lock.acquire() #锁数量count=2 print(self.name, 'actionB-gotA', time.ctime()) time.sleep(1) # lockA.release() # lockB.release() r_lock.release() #锁数量count=1 r_lock.release() #锁数量count=0 def run(self): self.actionA() self.actionB() if __name__ == '__main__': # lockA = threading.Lock() # lockB = threading.Lock() r_lock = threading.RLock() thread_list = [] for i in range(5): t = MyThread() #创建5个线程 t.start() thread_list.append(t) for t in thread_list: t.join() print("ending the main thread!")
条件变量同步(Condition) ---有点麻烦不怎么用的
有一类线程需要满足条件之后才能够继续执行,Python提供了threading.Condition 对象用于条件变量线程的支持,它除了能提供RLock()或Lock()的方法外,
还提供了 wait()、notify()、notifyAll()方法。
lock_con=threading.Condition([Lock/Rlock]): 锁是可选选项,不传入锁,对象自动创建一个RLock()。
wait() :条件不满足时调用,线程会释放锁并进入等待阻塞;
notify() :条件创造后调用,通知等待池激活一个线程;
notifyAll() :条件创造后调用,通知等待池激活所有线程。
实例
同步条件(Event)
线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。
对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。
一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行条件同步和条件变量同步差不多意思,只是少了锁功能。
event=threading.Event():条件环境对象,初始值 为False
event.isSet():返回event的状态值;
event.wait():如果 event.isSet()==False将阻塞线程;
event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
event.clear():恢复event的状态值为False。
案例:
import threading,time class Boss(threading.Thread): def run(self): print("BOSS:今晚大家都要加班到22:00。",time.ctime()) print(event.isSet()) #False event.isSet() or event.set() time.sleep(5) print("BOSS:<22:00>可以下班了。",time.ctime()) event.isSet() or event.set() class Worker(threading.Thread): def run(self): event.wait() #一旦event被设定 pass print("Worker:哎……命苦啊!") time.sleep(0.25) event.clear() #清除event event.wait() print("Worker:OhYeah!") if __name__=="__main__": event=threading.Event() threads=[] for i in range(5): threads.append(Worker()) threads.append(Boss()) for t in threads: t.start() for t in threads: t.join() print("end。。。")
import threading,time import random def light(): if not event.isSet(): event.set() #wait就不阻塞 #绿灯状态 count = 0 while True: if count < 10: print('