day9-python-进程、线程和协程
一、进程 程序的执行实例称为进程。 每个进程提供执行程序所需的资源。进程有虚拟地址空间、可执行代码、系统对象的打开句柄、安全上下文、惟一进程标识符、环境变量、优先级类、最小和最大工作集大小,以及至少一个执行线程。每个进程由一个线程(通常称为主线程)启动,但是可以从它的任何线程创建额外的线程。 程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。 在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。 有了进程为什么还要线程?进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:
例如,我们在使用qq聊天, qq做为一个独立进程如果同一时间只能干一件事,那他如何实现在同一时刻 即能监听键盘输入、又能监听其它人给你发的消息、同时还能把别人发的消息显示在屏幕上呢?你会说,操作系统不是有分时么?但我的亲,分时是指在不同进程间的分时呀, 即操作系统处理一会你的qq任务,又切换到word文档任务上了,每个cpu时间片分给你的qq程序时,你的qq还是只能同时干一件事呀。 再直白一点, 一个操作系统就像是一个工厂,工厂里面有很多个生产车间,不同的车间生产不同的产品,每个车间就相当于一个进程,且你的工厂又穷,供电不足,同一时间只能给一个车间供电,为了能让所有车间都能同时生产,你的工厂的电工只能给不同的车间分时供电,但是轮到你的qq车间时,发现只有一个干活的工人,结果生产效率极低,为了解决这个问题,应该怎么办呢?。。。。没错,你肯定想到了,就是多加几个工人,让几个人工人并行工作,这每个工人,就是线程! 二、线程 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务 线程是一个执行上下文,它是CPU执行指令流所需的所有信息。 假设你正在读一本书,你现在想休息一下,但是你想从你停止的地方回来继续阅读。一种方法是记下页码、行号和字数。你阅读一本书的执行环境是这三个数字。 如果你有一个室友,她用了同样的方法,她可以在你不用的时候拿起这本书,从她停下的地方开始读。然后你可以把它拿回去,从你原来的地方继续。 线程以相同的方式工作。CPU给你的错觉是它在同一时间做多个计算。它通过在每次计算上花费一点时间来实现这一点。它可以这样做,因为它对每个计算都有一个执行上下文。就像你可以和朋友共享一本书一样,许多任务也可以共享一个CPU。 在更技术的层面上,执行上下文(因此是线程)由CPU寄存器的值组成。 最后:线程与进程不同。线程是执行的上下文,而进程是一组与计算相关的资源。一个进程可以有一个或多个线程。所有在同一个进程里的线程是共享同一块内存空间的。 说明:与进程相关的资源包括内存页(进程中的所有线程都具有相同的内存视图)、文件描述符(例如open sockets)和安全凭据(例如,启动进程的用户的ID)。 三、进程和线程的区别 1、线程共享内存空间,进程的内存空间是独立的。 2、线程可以直接访问其进程的数据段;进程有自己的父进程数据段的副本。 3、同一个进程的线程之间可以直接交流,两个进程想通信,必须通过一个中间代理来实现。 4、创建新线程很简单,创建新进程需要对其父进程进行一次克隆。 5、一个线程可以控制和操作同一进程里的其他线程,但是进程只能操作子进程。 6、对主线程的修改可能影响到其他线程,对父进程的修改不会影响子进程。 四、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在执行的时候会淡定的在同一时刻只允许一个线程运行,擦。。。,那这还叫什么多线程呀?莫如此早的下结结论,听我现场讲。 首先需要明确的一点是 五、Python threading模块 线程有两种调用方式,如下: 直接调用 #!/usr/bin/env python # -*- coding:utf-8 -*- import threading import time def run(n): print("task",n) time.sleep(2) if __name__ == ‘__main__‘: t1 = threading.Thread(target=run,args=(1,)) #生成一个线程实例 t2 = threading.Thread(target=run,args=(2,)) #生成另一个线程实例 t1.start() #启动线程 t2.start() #启动另一个线程 print(t1.getName()) #获取线程名 print(t2.getName()) 继承式调用 #!/usr/bin/env python # -*- coding:utf-8 -*- import threading import time class MyThread(threading.Thread): def __init__(self,n): super(MyThread,self).__init__() self.n = n def run(self): #定义每个线程要运行的函数 print("running task",self.n) time.sleep(3) if __name__ == ‘__main__‘: t1 = MyThread(1) t2 = MyThread(2) t1.start() t2.start() 六、join & Daemon 有些线程执行后台任务,比如发送keepalive数据包,或者执行周期性的垃圾收集,等等。这些只有在主程序运行时才有用,并且当其他非守护进程的线程退出时,可以终止它们。 如果没有守护进程线程,您必须跟踪它们,并在程序完全退出之前告诉它们退出。通过将它们设置为守护线程,您可以让它们运行并忘记它们,当您的程序退出时,任何守护线程都会自动被杀死。 #!/usr/bin/env python # -*- coding:utf-8 -*- import threading import time def run(n): print("task",n) time.sleep(2) print("task done",n,threading.current_thread()) start_time = time.time() t_objs = [] #存线程实例 for i in range(20): t = threading.Thread(target=run,args=("t-%s" %i,)) t.start() t_objs.append(t) #为了不阻塞后面线程的启动,不在这里join,先放到一个列表 for t in t_objs: #循环线程实例列表,等待所有线程执行完毕 t.join() print("-----------------all threads has finished...",threading.current_thread(),threading.active_count()) print("cost:",time.time() - start_time) #!/usr/bin/env python # -*- coding:utf-8 -*- import threading import time def run(n): print("task",)) t.setDaemon(True) #把当前线程设置成守护线程 t.start() t_objs.append(t) print("-----------------all threads has finished...",time.time() - start_time) 注意:守护进程线程在关闭时突然停止。它们的资源(如打开的文件、数据库事务等)可能无法正确释放。如果希望线程优雅地停止,请使它们非守护进程,并使用适当的信号机制(如事件)。 七、线程锁(互斥锁Mutex) 一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据,会出现什么状况? #!/usr/bin/env python # -*- coding:utf-8 -*- import threading import time
正常来讲,这个num结果应该是0, 但在python 2.7上多运行几次,会发现,最后打印出来的num结果不总是0,为什么每次运行的结果不一样呢? 哈,很简单,假设你有A,B两个线程,此时都 要对num 进行减1操作, 由于2个线程是并发同时运行的,所以2个线程很有可能同时拿走了num=100这个初始变量交给cpu去运算,当A线程去处完的结果是99,但此时B线程运算完的结果也是99,两个线程同时CPU运算的结果再赋值给num变量后,结果就都是99。那怎么办呢? 很简单,每个线程在要修改公共数据时,为了避免自己在还没改完的时候别人也来修改此数据,可以给这个数据加一把锁, 这样其它线程想修改此数据时就必须等待你修改完毕并把锁释放掉后才能再访问此数据。? *注:不要在3.x上运行,不知为什么,3.x上的结果总是正确的,可能是自动加了锁 加锁版本 import time import threading def addNum(): global num # 在每个线程中都获取这个全局变量 print(‘--get num:‘,num) time.sleep(1) lock.acquire() # 修改数据前加锁 num -= 1 # 对此公共变量进行-1操作 lock.release() # 修改后释放 num = 100 # 设定一个共享变量 thread_list = [] lock = threading.Lock() # 生成全局锁 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) 八、GIL VS Lock 机智的同学可能会问到这个问题,就是既然你之前说过了,Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock? 注意啦,这里的lock是用户级的lock,跟那个GIL没关系 ,具体我们通过下图来看一下+配合我现场讲给大家,就明白了。 那你又问了, 既然用户程序已经自己有锁了,那为什么C python还需要GIL呢?加入GIL主要的原因是为了降低程序的开发的复杂度,比如现在的你写python不需要关心内存回收的问题,因为Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时你自己的程序 里的线程和 py解释器自己的线程是并发运行的,假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题, ?这可以说是Python早期版本的遗留问题。 九、RLock(递归锁) 说白了就是在一个大锁中还要再包含子锁 #!/usr/bin/env python # -*- coding:utf-8 -*- import threading,time def run1(): print("grab the first part data") lock.acquire() global num num += 1 lock.release() return num def run2(): print("grab the second part data") lock.acquire() global num2 num2 += 1 lock.release() return num2 def run3(): lock.acquire() res = run1() print(‘--------between run1 and run2-----‘) res2 = run2() lock.release() print(res,res2) num,num2 = 0,0 lock = threading.RLock() for i in range(1): t = threading.Thread(target=run3) t.start() while threading.active_count() != 1: print(threading.active_count()) else: print(‘----all threads done---‘) print(num,num2) 十、Semaphore(信号量) 互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。 #!/usr/bin/env python # -*- coding:utf-8 -*- import threading,time def run(n): semaphore.acquire() time.sleep(1) print("run the thread: %sn" % n) semaphore.release() if __name__ == ‘__main__‘: semaphore = threading.BoundedSemaphore(5) # 最多允许5个线程同时运行 for i in range(22): t = threading.Thread(target=run,args=(i,)) t.start() while threading.active_count() != 1: pass # print threading.active_count() else: print(‘----all threads done---‘) #print(num) 十一、Timer该类表示仅在经过一定时间后才应运行的操作 与线程一样,通过调用它们的start()方法来启动计时器。可以通过调用thecancel()方法来停止计时器(在其操作开始之前)。计时器在执行其操作之前将等待的时间间隔可能与用户指定的时间间隔不完全相同。 def hello(): print("hello,world") t = Timer(30.0,hello) t.start() # after 30 seconds,"hello,world" will be printed 十二、Events 事件是一个简单的同步对象; 事件表示内部标志和线程 可以等待旗子被设置,或者自己设置或清除旗子。 event = threading.Event() 客户端线程可以等待设置标记 event.wait() 服务器线程可以设置或重置它 event.set() 如果设置了该标志,那么wait方法将不执行任何操作。 如果标志被清除,wait将被阻塞,直到它再次被设置。 任意数量的线程可以等待同一个事件。 通过Event来实现两个或多个线程间的交互,下面是一个红绿灯的例子,即起动一个线程做交通指挥灯,生成几个线程做车辆,车辆行驶按红灯停,绿灯行的规则。 #!/usr/bin/env python # -*- coding:utf-8 -*- import time import threading event = threading.Event() def lighter(): count = 0 event.set() #先设为绿灯 while True: if count > 5 and count <10: #改成红灯 event.clear() #把标志位请了 print(" |