主要内容:
1、进程、线程区别
2、python GIL全局解释器锁
进程与线程
什么是进程?
程序的执行实例称为进程。每个过程都提供执行程序所需的资源。进程具有虚拟地址空间、可执行代码、对系统对象的打开句柄、安全上下文、惟一进程标识符、环境变量、优先级类、最小和最大工作集大小,以及至少一个执行线程。每个进程都由一个线程启动,通常称为主线程,但是可以从它的任何线程创建额外的线程。
程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。
在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。
有了进程为什么还要线程?
进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:
-
进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
-
进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
例如,我们在使用qq聊天, qq做为一个独立进程如果同一时间只能干一件事,那他如何实现在同一时刻 即能监听键盘输入、又能监听其它人给你发的消息、同时还能把别人发的消息显示在屏幕上呢?你会说,操作系统不是有分时么?但我的亲,分时是指在不同进程间的分时呀, 即操作系统处理一会你的qq任务,又切换到word文档任务上了,每个cpu时间片分给你的qq程序时,你的qq还是只能同时干一件事呀。
什么是线程?
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程是一个执行上下文,它是CPU执行指令流所需的所有信息。
假设你正在阅读一本书,你现在想休息一下,但是你希望能够从你停止阅读的地方重新开始阅读。实现这一点的一种方法是记下页码、行号和单词号。所以你阅读一本书的执行上下文是这三个数字。
如果你有一个室友,她也在用同样的方法,她可以在你不用的时候拿起这本书,从她停下的地方继续阅读。然后你就可以把它拿回去,重新开始。
线程以同样的方式工作。CPU给你的错觉是它同时在做多个计算。它通过在每次计算上花费一点时间来做到这一点。它可以这样做,因为它为每个计算都有一个执行上下文。就像您可以与您的朋友共享一本书一样,许多任务也可以共享一个CPU。
在更技术的层次上,执行上下文(因此是线程)由CPU寄存器的值组成。
最后:线程与进程不同。线程是执行的上下文,而进程是一组与计算相关的资源。一个进程可以有一个或多个线程。
说明:与进程关联的资源包括内存页(进程中的所有线程都具有相同的内存视图)、文件描述符(例如open sockets)和安全凭据(例如,启动进程的用户的ID)。
进程和线程的区别:
1、线程共享创建进程的地址空间;进程有自己的地址空间。
2、线程可以直接访问其进程的数据段;进程拥有父进程的数据段的自己的副本。
3、线程可以直接与所在进程中的其他线程通信;进程必须使用进程间通信来与同级进程通信。
4、新线程很容易创建;新进程需要父进程的复制。
5、线程可以对同一进程的线程进行相当大的控制;进程只能对子进程进行控制。
6、主线程的更改(取消、优先级更改等)可能会影响进程中其他线程的行为;对父进程的更改不影响子进程。
python GIL(全局解释器锁)
在CPython中,全局解释器锁(global interpreter lock, GIL)是一个互斥体,它防止多个本机线程同时执行Python字节码。这个锁是必要的,主要是因为CPython的内存管理不是线程安全的。(然而,自从GIL存在以来,其他特性已经逐渐依赖于它强制执行的保证。
即是:无论你启多少个线程,你有多少个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。
Threading模块
线程有两种调用方式:直接调用、继承式调用
直接调用:
import threading import time def sayhi(num): #定义每个线程要运行的函数 print("running on number:%s" %num) time.sleep(1) 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())
继承式调用:
import threading import time class MyThread(threading.Thread): def __init__(self,num): threading.Thread.__init__(self) self.num = num def run(self):#定义每个线程要运行的函数 print("running on number:%s" %self.num) time.sleep(1) if __name__ == '__main__': t1 = MyThread(1) t2 = MyThread(2) t1.start() t2.start()
Join & Daemon
有些线程执行后台任务,比如发送keepalive数据包,或执行定期垃圾收集,等等。这些只有在主程序运行时才有用,一旦其他非守护进程线程退出,就可以终止它们。
如果没有守护进程线程,您必须跟踪它们,并在程序完全退出之前告诉它们退出。通过将它们设置为守护进程线程,您可以让它们运行并忘记它们,当程序退出时,任何守护进程线程都会自动被杀死。
即主线程退出时,守护进程自动退出。
import time import threading def run(n): print('[%s]------running----\n' % n) time.sleep(2) print('--done--') def main(): for i in range(5): t = threading.Thread(target=run,args=[i,]) t.start() t.join(1) print('starting thread', t.getName()) m = threading.Thread(target=main,args=[]) m.setDaemon(True) #将main线程设置为Daemon线程,它做为程序主线程的守护线程,当主线程退出时,m线程也会退出,由m启动的其它子线程会同时退出,不管是否执行完任务 m.start() m.join(timeout=2) print("---main thread done----")
注意:守护程序线程突然停在关闭。它们的资源(如打开的文件、数据库事务等)可能无法正确释放。如果希望线程优雅地停止,请使它们非守护进程,并使用适当的信号机制(如Event)
线程锁(互斥锁Mutex)
一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据,会出现什么状况?
import time import threading def addNum(): global num #在每个线程中都获取这个全局变量 print('--get num:',num ) time.sleep(1) num -=1 #对此公共变量进行-1操作 num = 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 )
正常来讲,这个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(递归锁)
即是大锁中含有子锁,锁的嵌套
#Author:Dylan import threading import time lock = threading.Lock() num = 0 def run(n): lock.acquire() global num num += 1 #time.sleep(1) lock.release() t_objs = [] for i in range(50): t = threading.Thread(target=run,args=("t-%s" %i,)) t.start() t_objs.append(t) for t in t_objs: t.join() print("---------all threads has finished...",threading.current_thread(),threading.active_count()) print("num:",num)
Semaphore(信号量)
互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据
import threading,time def run(n): semaphore.acquire() time.sleep(1) print("run the thread: %s\n" %n) semaphore.release() if __name__ == '__main__': num= 0 semaphore = threading.BoundedSemaphore(5) #最多允许5个线程同时运行 for i in range(20): 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()
event.clear()
如果设置了该标志,wait方法将不执行任何操作。
如果清除了标志,wait将阻塞,直到再次设置为止。
任何数量的线程都可以等待相同的事件。
通过Event来实现两个或多个线程间的交互,下面是一个红绿灯的例子,即起动一个线程做交通指挥灯,生成几个线程做车辆,车辆行驶按红灯停,绿灯行的规则。
#Author:AZ import time import threading event = threading.Event() def lighter(): count = 0 event.set()#先设置绿灯 while True: if count>5 and count<10:#改成红灯 event.clear()#把标志清了 print("\033[41;1mred light is on...\033[0m") elif count>10: event.set()#变绿灯 count = 0 else: print("\033[42;1mgreen light is on...\033[0m") time.sleep(1) count +=1 def car(name): while True: if event.is_set(): #代表绿灯 print("[%s] running..."%name) time.sleep(1) else: print("[%s] sees red light ,waiting..."%name) event.wait() print("\033[34;1m[%s] green light is on,start going...\033[0m"%name) light = threading.Thread(target=lighter,) light.start() car1 = threading.Thread(target=car,args=("Tesla",)) car1.start()
队列(queue)
当必须在多个线程之间安全地交换信息时,队列在线程编程中尤其有用。
- class
queue.
Queue
(maxsize=0) #先入先出
- class
queue.
LifoQueue
(maxsize=0) #last in fisrt out - class
queue.
PriorityQueue
(maxsize=0) #存储数据时可设置优先级的队列
优先级队列的构造函数。maxsize是一个整数,它设置可以放置在队列中的项数的上限。一旦达到此大小,插入将阻塞,直到使用队列项。如果maxsize小于或等于零,则队列大小为无穷大。首先检索值最低的条目(值最低的条目是由已排序(列表(条目))[0]返回的条目)。条目的典型模式是元组的形式:(priority_number, data)。
#Author:AZ import queue # q = queue.PriorityQueue() # # q.put((-1,"chne")) # q.put((3,"az")) # q.put((10,"alex")) # q.put((6,"dylan")) # # # # print(q.get()) # print(q.get()) # print(q.get()) # print(q.get()) q = queue.LifoQueue() q.put(1) q.put(2) q.put(3) print(q.get()) print(q.get()) print(q.get())
exception queue.
Empty
在空队列对象上调用非阻塞get()(或get_nowait())时引发异常。
- exception
queue.
Full
当对已满的队列对象调用非阻塞put()(或put_nowait())时引发异常。
-
Queue.
qsize
()
-
Queue.
empty
() #return True if empty
-
Queue.
full
() # return True if full
-
Queue.
put
(item, block=True, timeout=None)
将项目放入队列中。如果可选的args块为true, timeout为None(缺省值),则在空闲插槽可用之前,如果有必要,将阻塞。如果timeout是一个正数,那么它将阻塞最多的超时秒,如果在这段时间内没有可用的空闲插槽,则引发完整的异常。否则(block为false),如果一个空闲插槽立即可用,则将一个项放到队列中,否则引发完全异常(在这种情况下忽略超时)。
-
Queue.
put_nowait
(item) #=get(False)
提供了两种方法来支持跟踪已加入队列的任务是否已被守护进程使用者线程完全处理。
-
Queue.
task_done
()
指示已完成先前排队的任务。由队列使用者线程使用。对于用于获取任务的每个get(),后续对task_done()的调用告诉队列任务上的处理已经完成。
如果join()当前处于阻塞状态,那么在处理完所有项之后,它将继续运行(这意味着对于已经放入队列()的每个项,都收到了task_done()调用)。
如果调用次数超过放置在队列中的项的次数,则引发ValueError。
-
Queue.
join
() block直到queue被消费完毕
生产者消费者模型
为什么要使用生产者和消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
什么是生产者消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
基本的生产者消费者模型的例子:
#Author:AZ import threading,time import queue q = queue.Queue(maxsize=10) def Producer(name): count = 1 while True: q.put("骨头%s"%count) print("生产了骨头",count) count +=1 time.sleep(1) def Consumer(name): #while q.size()>0: while True: print("[%s] 取到了 [%s] 并且吃了它..." %(name,q.get())) time.sleep(1) p = threading.Thread(target=Producer,args=("alex",)) c = threading.Thread(target=Consumer,args=("chen",)) c1 = threading.Thread(target=Consumer,args=("wangsen",)) p.start() c.start() c1.start()