Python3 --- 进程和线程
一、进程概述 --- 百度百科
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
二、线程概述 --- 百度百科
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
三、进程与线程的区别
(1)进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元
(2)同一个进程中可以包括多个线程,并且线程共享整个进程的资源(寄存器、堆栈、上下文),一个进程至少包括一个线程。
(3)进程的创建调用fork或者vfork,而线程的创建调用pthread_create,进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束
(4)线程是轻两级的进程,它的创建和销毁所需要的时间比进程小很多,所有操作系统中的执行功能都是创建线程去完成的
(5)线程中执行时一般都要进行同步和互斥,因为他们共享同一进程的所有资源
(6)线程有自己的私有属性TCB,线程id,寄存器、硬件上下文,而进程也有自己的私有属性进程控制块PCB,这些私有属性是不被共享的,用来标示一个进程或一个线程的标志
进程的几种状态:
(1)run(运行状态):正在运行的进程或在等待队列中对待的进程,等待的进程只要以得到cpu就可以运行(2)Sleep(可中断休眠状态):相当于阻塞或在等待的状态
(3)D(不可中断休眠状态):在磁盘上的进程
(4)T(停止状态):这中状态无法直观的看见,因为是进程停止后就释放了资源,所以不会留在linux中
(5)Z(僵尸状态):子进程先与父进程结束,但父进程没有调用wait或waitpid来回收子进程的资源,所以子进程就成了僵尸进程,如果父进程结束后任然没有回收子进程的资源,那么1号进程将回收
四、Python3进程的创建方式
4.1、fork()
Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getpid()就可以拿到父进程的ID。
Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程:
import os rpid = os.fork() if rpid<0: print("fork调用失败。") elif rpid == 0: print("我是子进程(%s),我的父进程是(%s)"%(os.getpid(),os.getppid())) x+=1 else: print("我是父进程(%s),我的子进程是(%s)"%(os.getpid(),rpid)) print("父子进程都可以执行这里的代码")
注:
(1)由于Windows没有fork调用,上面的代码在Windows上无法运行。
(2)有了fork调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork出子进程来处理新的http请求。
多次调用fork()的问题,代码如下:
import os import time # 注意,fork函数,只在Unix/Linux/Mac上运行,windows不可以 pid = os.fork() if pid == 0: print('fork1子进程') else: print('fork1父进程') pid = os.fork() if pid == 0: print('fork2子进程') else: print('fork2父进程') time.sleep(1)
输出如下:
fork1父进程 fork2子进程 fork2父进程 fork1子进程 fork2父进程 fork2子进程
从上输出可以看出第二次通过fork()创建进程时,生产了4个进程,这是由于在第二次fork()时,是基于第一次的fork()创建出来的父进程、子进程再次独立创建父进程、子进程,如图:
4.2、multiprocessing
multiprocessing是一个使用类似于threading模块的API来支持多进程。该multiprocessing软件包提供本地和远程并发,通过使用子进程来充分利用服务器上的多个处理器,它可以在Unix和Windows上运行。
multiprocessing模块提供了一个Process类来代表一个进程对象,示例如下:
from multiprocessing import Process def proce(pn): print('hello',pn) if __name__ == '__main__': for i in range(10): #启动10个进程 p1 = Process(target=proce,args=(i,),name=str(i)) #创建进程 p1.start() #启动进程 p1.join() #等待进程结束 print('exit project')
Process语法结构如下:
Process([group [, target [, name [, args [, kwargs]]]]])
- target:表示这个进程实例所调用对象;
- args:表示调用对象的位置参数元组;
- kwargs:表示调用对象的关键字参数字典;
- name:为当前进程实例的别名;
- group:大多数情况下用不到;
Process类常用方法:
- is_alive():判断进程实例是否还在执行;
- join([timeout]):是否等待进程实例执行结束,或等待多少秒;
- start():启动进程实例(创建子进程);
- run():如果没有给定target参数,对这个对象调用start()方法时,就将执行对象中的run()方法;
- terminate():不管任务是否完成,立即终止;
Process类常用属性:
- name:当前进程实例别名,默认为Process-N,N为从1开始递增的整数;
- pid:当前进程实例的PID值;
4.3、Pool --- 进程池
当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process动态成生多个进程,但如果是上百甚至上千个目标,手动的去创建进程的工作量巨大,此时就可以用到multiprocessing模块提供的Pool方法。
初始化Pool时,可以指定一个最大进程数,当有新的请求提交到Pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到指定的最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来执行,请看下面的实例:
from multiprocessing import Pool import os,time,random def worker(msg): t_start = time.time() print("%s开始执行,进程号为%d"%(msg,os.getpid())) #random.random()随机生成0~1之间的浮点数 time.sleep(random.random()*2) t_stop = time.time() print(msg,"执行完毕,耗时%0.2f"%(t_stop-t_start)) po=Pool(3) #定义一个进程池,最大进程数3 for i in range(0,10): #Pool.apply_async(要调用的目标,(传递给目标的参数元祖,)) #每次循环将会用空闲出来的子进程去调用目标 po.apply_async(worker,(i,)) print("----start----") po.close() #关闭进程池,关闭后po不再接收新的请求 po.join() #等待po中所有子进程执行完成,必须放在close语句之后 print("-----end-----")
运行结果:
----start---- 0开始执行,进程号为21466 1开始执行,进程号为21468 2开始执行,进程号为21467 0 执行完毕,耗时1.01 3开始执行,进程号为21466 2 执行完毕,耗时1.24 4开始执行,进程号为21467 3 执行完毕,耗时0.56 5开始执行,进程号为21466 1 执行完毕,耗时1.68 6开始执行,进程号为21468 4 执行完毕,耗时0.67 7开始执行,进程号为21467 5 执行完毕,耗时0.83 8开始执行,进程号为21466 6 执行完毕,耗时0.75 9开始执行,进程号为21468 7 执行完毕,耗时1.03 8 执行完毕,耗时1.05 9 执行完毕,耗时1.69 -----end-----
multiprocessing.Pool常用函数解析:
- apply_async(func[, args[, kwds]]) :使用非阻塞方式调用func(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args为传递给func的参数列表,kwds为传递给func的关键字参数列表;
- apply(func[, args[, kwds]]):使用阻塞方式调用func
- close():关闭Pool,使其不再接受新的任务;
- terminate():不管任务是否完成,立即终止;
- join():主进程阻塞,等待子进程的退出, 必须在close或terminate之后使用;
apply堵塞式
from multiprocessing import Pool import os,time,random def worker(msg): t_start = time.time() print("%s开始执行,进程号为%d"%(msg,os.getpid())) #random.random()随机生成0~1之间的浮点数 time.sleep(random.random()*2) t_stop = time.time() print(msg,"执行完毕,耗时%0.2f"%(t_stop-t_start)) po=Pool(3) #定义一个进程池,最大进程数3 for i in range(0,10): po.apply(worker,(i,)) print("----start----") po.close() #关闭进程池,关闭后po不再接收新的请求 po.join() #等待po中所有子进程执行完成,必须放在close语句之后 print("-----end-----")
运行结果:
0开始执行,进程号为21532 0 执行完毕,耗时1.91 1开始执行,进程号为21534 1 执行完毕,耗时1.72 2开始执行,进程号为21533 2 执行完毕,耗时0.50 3开始执行,进程号为21532 3 执行完毕,耗时1.27 4开始执行,进程号为21534 4 执行完毕,耗时1.05 5开始执行,进程号为21533 5 执行完毕,耗时1.60 6开始执行,进程号为21532 6 执行完毕,耗时0.25 7开始执行,进程号为21534 7 执行完毕,耗时0.63 8开始执行,进程号为21533 8 执行完毕,耗时1.21 9开始执行,进程号为21532 9 执行完毕,耗时0.60 ----start---- -----end-----
4.4、Queue --- 进程间通信
可以使用multiprocessing模块的Queue实现多进程之间的数据传递,Queue本身是一个消息列队程序,首先用一个小实例来演示一下Queue的工作原理:
#coding=utf-8 from multiprocessing import Queue q=Queue(3) #初始化一个Queue对象,最多可接收三条put消息 print(q.empty()) #判断queue是否是空 q.put("消息1") q.put("消息2") print(q.qsize()) #获取queue中的消息数量 print(q.full()) #判断queue是否已满 q.put("消息3") print(q.qsize()) #获取queue中的消息数量 print(q.full()) #判断queue是否已满 #因为消息列队已满下面的try都会抛出异常,第一个try会等待2秒后再抛出异常,第二个Try会立刻抛出异常 try: q.put("消息4",True,2) except: print("消息列队已满,现有消息数量:%s"%q.qsize()) try: q.put_nowait("消息4") except: print("消息列队已满,现有消息数量:%s"%q.qsize()) #推荐的方式,先判断消息列队是否已满,再写入 if not q.full(): q.put_nowait("消息4") #存储不用等待,如果queue已满,则抛出异常 #读取消息时,先判断消息列队是否为空,再读取 if not q.empty(): for i in range(q.qsize()): print(q.get_nowait()) #读取不用等待,如果queue为空,则抛出异常
输出结果:
True 2 False 3 True 消息列队已满,现有消息数量:3 消息列队已满,现有消息数量:3 消息1 消息2 消息3
说明:
初始化Queue()对象时(例如:q=Queue()),若括号中没有指定最大可接收的消息数量,或数量为负值,那么就代表可接受的消息数量没有上限(直到内存的尽头);
- Queue.qsize():返回当前队列包含的消息数量;
- Queue.empty():如果队列为空,返回True,反之False ;
- Queue.full():如果队列满了,返回True,反之False;
- Queue.get([block[, timeout]]):获取队列中的一条消息,然后将其从列队中移除,block默认值为True;
1)如果block使用默认值,且没有设置timeout(单位秒),消息列队如果为空,此时程序将被阻塞(停在读取状态),直到从消息列队读到消息为止,如果设置了timeout,则会等待timeout秒,若还没读取到任何消息,则抛出"Queue.Empty"异常;
2)如果block值为False,消息列队如果为空,则会立刻抛出"Queue.Empty"异常;
- Queue.get_nowait():相当Queue.get(False);
- Queue.put(item,[block[, timeout]]):将item消息写入队列,block默认值为True;
1)如果block使用默认值,且没有设置timeout(单位秒),消息列队如果已经没有空间可写入,此时程序将被阻塞(停在写入状态),直到从消息列队腾出空间为止,如果设置了timeout,则会等待timeout秒,若还没空间,则抛出"Queue.Full"异常;
2)如果block值为False,消息列队如果没有空间可写入,则会立刻抛出"Queue.Full"异常;
- Queue.put_nowait(item):相当Queue.put(item, False);
Queue与进程
我们以Queue为例,在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据:
from multiprocessing import Process, Queue import os, time, random # 写数据进程执行的代码: def write(q): for value in ['A', 'B', 'C']: print('Put %s to queue...' % value) q.put(value) time.sleep(random.random()) # 读数据进程执行的代码: def read(q): while True: if not q.empty(): value = q.get(True) print('Get %s from queue.' % value) time.sleep(random.random()) else: break if __name__=='__main__': # 父进程创建Queue,并传给各个子进程: q = Queue() pw = Process(target=write, args=(q,)) pr = Process(target=read, args=(q,)) # 启动子进程pw,写入: pw.start() # 等待pw结束: pw.join() # 启动子进程pr,读取: pr.start() pr.join() # pr进程里是死循环,无法等待其结束,只能强行终止: print('') print('所有数据都写入并且读完')
输出:
Put A to queue... Put B to queue... Put C to queue... Get A from queue. Get B from queue. Get C from queue. 所有数据都写入并且读完
Queue与进程池
如果要使用Pool创建进程,就需要使用multiprocessing.Manager()中的Queue(),而不是multiprocessing.Queue(),否则会得到一条如下的错误信息:
RuntimeError: Queue objects should only be shared between processes through inheritance.
下面的实例演示了进程池中的进程如何通信:
#coding=utf-8 #修改import中的Queue为Manager from multiprocessing import Manager,Pool import os,time,random def reader(q): print("reader启动(%s),父进程为(%s)"%(os.getpid(),os.getppid())) for i in range(q.qsize()): print("reader从Queue获取到消息:%s"%q.get(True)) def writer(q): print("writer启动(%s),父进程为(%s)"%(os.getpid(),os.getppid())) for i in "dongGe": q.put(i) if __name__=="__main__": print("(%s) start"%os.getpid()) q=Manager().Queue() #使用Manager中的Queue来初始化 po=Pool() #使用阻塞模式创建进程,这样就不需要在reader中使用死循环了,可以让writer完全执行完成后,再用reader去读取 po.apply(writer,(q,)) po.apply(reader,(q,)) po.close() po.join() print("(%s) End"%os.getpid())
运行结果:
(11868) start writer启动(9912),父进程为(11868) reader启动(9912),父进程为(11868) reader从Queue获取到消息:d reader从Queue获取到消息:o reader从Queue获取到消息:n reader从Queue获取到消息:g reader从Queue获取到消息:G reader从Queue获取到消息:e (11868) End
五、线程
Python3 通过两个标准库 _thread 和 threading 提供对线程的支持。
_thread 提供了低级别的、原始的线程以及一个简单的锁,它相比于 threading 模块的功能还是比较有限的。threading 模块除了包含 _thread 模块中的所有方法外,还提供的其他方法:
- threading.currentThread(): 返回当前的线程变量。
- threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
- threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
- run(): 用以表示线程活动的方法。
- start():启动线程活动。
- join([time]): 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
- isAlive(): 返回线程是否活动的。
- getName(): 返回线程名。
- setName(): 设置线程名。
- setDaemon():设置为后台线程或前台线程(默认)如果是后台线程,主线程执行过程中,后台线程也在执行,主线程执行完毕后,后台线程不论成功与否,均停止;如果是前台线程,主线程执行过程中,前台线程也在执行,主线程执行完毕后,等待前台线程也执行完成后,程序停止。
5.1、单线程执行:
#coding=utf-8 import time def say(): print("Hello World") time.sleep(1) if __name__ == "__main__": for i in range(5): say()
5.2、多线程:
#coding=utf-8 import threading import time def say(): print("Hello World") time.sleep(1) if __name__ == "__main__": for i in range(5): t = threading.Thread(target=say) t.start() #启动线程,即让线程开始执行
5.3、查看线程数量:
#coding=utf-8 import threading from time import sleep,ctime def sing(): for i in range(3): print("正在唱歌...%d"%i) sleep(1) def dance(): for i in range(3): print("正在跳舞...%d"%i) sleep(1) if __name__ == '__main__': print('---开始---:%s'%ctime()) t1 = threading.Thread(target=sing) t2 = threading.Thread(target=dance) t1.start() t2.start() while True: length = len(threading.enumerate()) print('当前运行的线程数为:%d'%length) if length<=1: break sleep(0.5)
5.4、继承式调用线程
通过使用threading模块能完成多任务的程序开发,为了让每个线程的封装性更完美,所以使用threading模块时,往往会定义一个新的子类class,只要继承threading.Thread就可以了,然后重写run方法
#!/usr/bin/env python #coding:utf8 import threading,time class mythreading(threading.Thread): #写一个类方法继承threading模块 def __init__(self): #threading.Thread.__init__(self) #金典类重写父类方法 super(mythreading,self).__init__() #重写父类属性 self.name = self.name.split('d')[1] def run(self): #运行线程的函数,函数名必须是run名称 super(mythreading,self).run() print('starting on threading',self.name) time.sleep(5) if __name__ == '__main__': #t1 = mythreading(1) #通过类创建线程 #t2 = mythreading(2) #t1.start() #启动进程 #t2.start() ttr = [] for i in range(10): #启动十个线程 t = mythreading() ttr.append(t) t.start() t.setName('hehe-{}'.format(i)) #修改线程名 print(t.getName()) #获取线程名 for item in ttr: item.join() #阻断线程等待执行完后再执行后续代码 print('-----end')
5.5、守护线程
#!/usr/bin/env python #coding:utf8 import time import threading def run(num): #子线程运行函数 print('---starting',num) time.sleep(2) print('---done') def main(): #主线程运行函数 print('开启主线程') for i in range(4): #在主线程中运行4个子线程 t1 = threading.Thread(target=run,args=(i,)) t1.start() print('启动线程',t1.getName()) t1.join() print('结束主线程') m = threading.Thread(target=main,args=()) m.setDaemon(True) #设置主线程为守护线程 m.start() m.join(timeout=3) #等待3秒后主线程退出,不管子线程是否运行完 print('------end')
5.6、线程的执行顺序
#coding=utf-8 import threading import time class MyThread(threading.Thread): def run(self): for i in range(3): time.sleep(1) msg = "I'm "+self.name+' @ '+str(i) print(msg) def test(): for i in range(5): t = MyThread() t.start() if __name__ == '__main__': test()
运行结果:
I'm Thread-3 @ 0 I'm Thread-1 @ 0 I'm Thread-4 @ 0 I'm Thread-2 @ 0 I'm Thread-5 @ 0 I'm Thread-5 @ 1 I'm Thread-2 @ 1 I'm Thread-1 @ 1 I'm Thread-3 @ 1 I'm Thread-4 @ 1 I'm Thread-3 @ 2 I'm Thread-2 @ 2 I'm Thread-1 @ 2 I'm Thread-4 @ 2 I'm Thread-5 @ 2
说明:
根据上面的运行结果可以看出,当线程创建成功后,线程的执行顺序是不确定的
5.7、线程的状态
5.7、线程共享全局变量
from threading import Thread import time g_num = 100 def work1(): global g_num for i in range(3): g_num += 1 print("----in work1, g_num is %d---"%g_num) def work2(): global g_num print("----in work2, g_num is %d---"%g_num) print("---线程创建之前g_num is %d---"%g_num) t1 = Thread(target=work1) t1.start() #延时一会,保证t1线程中的事情做完 time.sleep(1) t2 = Thread(target=work2) t2.start()
运行结果:
---线程创建之前g_num is 100--- ----in work1, g_num is 103--- ----in work2, g_num is 103---
from threading import Thread import time def work1(nums): nums.append(44) print("----in work1---",nums) def work2(nums): #延时一会,保证t1线程中的事情做完 time.sleep(1) print("----in work2---",nums) g_nums = [11,22,33] t1 = Thread(target=work1, args=(g_nums,)) t1.start() t2 = Thread(target=work2, args=(g_nums,)) t2.start()
运行结果:
----in work1--- [11, 22, 33, 44] ----in work2--- [11, 22, 33, 44]
5.8、线程同步 --- 互斥锁
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制,线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。互斥锁为资源引入一个状态:锁定/非锁定。
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
threading模块中定义了Lock类,可以方便的处理锁定:
#创建锁
mutex = threading.Lock()
#锁定
mutex.acquire([blocking])
#释放
mutex.release()
其中,锁定方法acquire可以有一个blocking参数。
如果设定blocking为True,则当前线程会堵塞,直到获取到这个锁为止(如果没有指定,那么默认为True)
如果设定blocking为False,则当前线程不会堵塞
使用互斥锁实现上面的例子的代码如下:
from threading import Thread, Lock import time g_num = 0 def test1(): global g_num for i in range(1000000): #True表示堵塞 即如果这个锁在上锁之前已经被上锁了,那么这个线程会在这里一直等待到解锁为止 #False表示非堵塞,即不管本次调用能够成功上锁,都不会卡在这,而是继续执行下面的代码 mutexFlag = mutex.acquire(True) if mutexFlag: g_num += 1 mutex.release() print("---test1---g_num=%d"%g_num) def test2(): global g_num for i in range(1000000): mutexFlag = mutex.acquire(True) #True表示堵塞 if mutexFlag: g_num += 1 mutex.release() print("---test2---g_num=%d"%g_num) #创建一个互斥锁 #这个所默认是未上锁的状态 mutex = Lock() p1 = Thread(target=test1) p1.start() p2 = Thread(target=test2) p2.start() print("---g_num=%d---"%g_num)
上锁解锁过程:
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。
每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。
线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
锁的优劣势:
(1)锁的好处:
- 确保了某段关键代码只能由一个线程从头到尾完整地执行
(2)锁的坏处:
- 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
- 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
5.9、线程同步 --- 条件判断
所谓条件变量,即这种机制是在满足了特定的条件后,线程才可以访问相关的数据。它使用Condition类来完成,由于它也可以像锁机制那样用,所以它也有acquire方法和release方法,而且它还有wait,notify,notifyAll方法。
""" 一个简单的生产消费者模型,通过条件变量的控制产品数量的增减,调用一次生产者产品就是+1,调用一次消费者产品就会-1. """ """ 使用 Condition 类来完成,由于它也可以像锁机制那样用,所以它也有 acquire 方法和 release 方法,而且它还有 wait, notify, notifyAll 方法。 """ import threading import queue, time, random class Goods: # 产品类 def __init__(self): self.count = 0 def add(self, num=1): self.count += num def sub(self): if self.count >= 0: self.count -= 1 def empty(self): return self.count <= 0 class Producer(threading.Thread): # 生产者类 def __init__(self, condition, goods, sleeptime=1): # sleeptime=1 threading.Thread.__init__(self) self.cond = condition self.goods = goods self.sleeptime = sleeptime def run(self): cond = self.cond goods = self.goods while True: cond.acquire() # 锁住资源 goods.add() print("产品数量:", goods.count, "生产者线程") cond.notifyAll() # 唤醒所有等待的线程--》其实就是唤醒消费者进程 cond.release() # 解锁资源 time.sleep(self.sleeptime) class Consumer(threading.Thread): # 消费者类 def __init__(self, condition, goods, sleeptime=2): # sleeptime=2 threading.Thread.__init__(self) self.cond = condition self.goods = goods self.sleeptime = sleeptime def run(self): cond = self.cond goods = self.goods while True: time.sleep(self.sleeptime) cond.acquire() # 锁住资源 while goods.empty(): # 如无产品则让线程等待 cond.wait() goods.sub() print("产品数量:", goods.count, "消费者线程") cond.release() # 解锁资源 g = Goods() c = threading.Condition() pro = Producer(c, g) pro.start() con = Consumer(c, g) con.start()
5.10、线程同步 --- Queue
该queue模块实现多生产者,多用户队列。当在多线程之间必须安全地交换信息时,它在线程编程中特别有用。该Queue模块中的类实现了所有必需的锁定语义,该模块实现三种类型的队列,它们的区别仅在于检索条目的顺序:
(1)在FIFO队列中,第一个添加的任务是第一个被检索的。
(2)在LIFO队列中,最后添加的条目是第一个被检索(像堆栈一样操作)。
(3)使用优先级队列,条目保持排序(使用heapq模块),并且首先检索最低值的条目。
生成者消费者模式:
(1)为什么要使用生产者和消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
(2)什么是生产者消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
(3)这个阻塞队列就是用来给生产者和消费者解耦的。纵观大多数设计模式,都会找一个第三者出来进行解耦(1)class queue.Queue(maxsize = 0 )
FIFO队列的构造器。 maxsize是一个整数,用于设置可放入队列的项目数的上限。一旦达到此大小,插入将会阻塞,直到队列项被消耗。如果 maxsize小于或等于零,队列大小是无限的。
(2)class queue.LifoQueue(maxsize = 0 )
LIFO队列的构造器。 maxsize是一个整数,用于设置可放入队列的项目数的上限。一旦达到此大小,插入将会阻塞,直到队列项被消耗。如果 maxsize小于或等于零,队列大小是无限的。
(3)class queue.PriorityQueue(maxsize = 0 )
优先队列的构造函数。 maxsize是一个整数,用于设置可放入队列的项目数的上限。一旦达到此大小,插入将会阻塞,直到队列项被消耗。如果 maxsize小于或等于零,队列大小是无限的。
首先检索最低值的条目(最低值条目是返回的条目sorted(list(entries))[0])。对于条目的典型图案的形式是一个元组:。(priority_number, data)
(1)exception queue.Empty
在空对象上调用非阻塞get()(或 get_nowait())时引发异常Queue。
(2)exception queue.Full
如果在已满的对象上调用非阻塞put()(或 put_nowait()),则会引发异常Queue。
队列对象(Queue,LifoQueue或PriorityQueue)提供以下的公共方法:
(1)Queue.qsize()
返回队列的大小。请注意,qsize()> 0并不保证后续的get()不会被阻塞,qsize()也不会保证put()不会被阻塞。
(2)Queue.empty()
如果队列为空则返回True,否则返回False。如果empty()返回True,则不保证对put()的后续调用不会被阻塞。同样,如果empty()返回False,则不保证后续调用get()不会被阻塞。
(3)Queue.full()
如果队列已满则返回True,否则返回Fales。如果full()返回True,则不保证后续的get()调用不会被阻塞。同样,如果full()返回False它并不能保证后续调用put()不会被阻塞。
(4)Queue.put(item,block = True,timeout = None )
将项目放入队列中。如果可选参数block为True,并且timeout为 None(默认),则根据需要进行阻止,直到空闲插槽可用。如果 timeout是一个正数,则最多会阻塞timeout秒数,Full如果在此时间内没有空闲插槽,则会引发异常。否则(block为false),如果一个空闲插槽立即可用,则在队列中放置一个项目,否则引发Full异常(在这种情况下timeout被忽略)。
(5)Queue.put_nowait(item )
相当于。put(item, False)
(6)Queue.get(block = True,timeout = None )
从队列中移除并返回一个项目。如果可选参数块为真,并且 timeout为None(默认),则在必要时阻塞,直到项目可用。如果timeout是一个正数,则最多会阻塞timeout秒数,Empty如果在该时间内没有可用项目,则会引发异常。否则(block为false),返回一个项目,如果一个是立即可用的,否则引发Empty异常(在这种情况下timeout被忽略)。
(7)Queue.get_nowait()
相当于get(False)。
(8)Queue.task_done()
表明以前排队的任务已经完成。由队列消费者线程使用。对于每个get()用于获取任务的对象,随后的调用都会task_done()告诉队列,任务的处理已完成。如果a join()当前被阻塞,则在处理所有项目时(意味着task_done()已经接收到已经put()进入队列的每个项目的呼叫),则将恢复。提出了一个ValueError好象叫更多的时间比中放入队列中的项目。
(9)Queue.join()
阻塞,直到队列中的所有项目都被获取并处理。每当将项目添加到队列中时,未完成任务的数量就会增加。只要消费者线程调用task_done()来指示该项目已被检索,并且所有工作都已完成,计数就会减少。当未完成任务的数量下降到零时,join()取消阻止。
import threading import queue import time import random ''' 1.创建一个 Queue.Queue() 的实例,然后使用数据对它进行填充。 2.将经过填充数据的实例传递给线程类,后者是通过继承 threading.Thread 的方式创建的。 3.每次从队列中取出一个项目,并使用该线程中的数据和 run 方法以执行相应的工作。 4.在完成这项工作之后,使用 queue.task_done() 函数向任务已经完成的队列发送一个信号。 5.对队列执行 join 操作,实际上意味着等到队列为空,再退出主程序。 ''' class jdThread(threading.Thread): def __init__(self,index,queue): threading.Thread.__init__(self) self.index = index self.queue = queue def run(self): while True: time.sleep(1) item = self.queue.get() if item is None: break print("序号:",self.index,"任务",item,"完成") self.queue.task_done()#task_done方法使得未完成的任务数量-1 q = queue.Queue(0) ''' 初始化函数接受一个数字来作为该队列的容量,如果传递的是 一个小于等于0的数,那么默认会认为该队列的容量是无限的. ''' for i in range(2): jdThread(i,q).start()#两个线程同时完成任务 for i in range(10): q.put(i)#put方法使得未完成的任务数量+1
先进先出队列
# Queue先进先出队列 import queue def show(q, i): if q.empty() or q.qsize() >= 1: q.put(i) # 存队列 elif q.full(): print('queue not size') que = queue.Queue(5) # 允许5个队列的队列对象 for i in range(5): show(que, i) print('queue is number:', que.qsize()) # 队列元素个数 for j in range(5): print(que.get()) # 取队列 print('......end')
先进后出队列
#LifoQueue先进后出队列 import queue lifoque = queue.LifoQueue() lifoque.put('hello1') lifoque.put('hello2') lifoque.put('hello3') print(lifoque.qsize()) print(lifoque.get()) print(lifoque.get()) print(lifoque.get())
按数据大小取最小值优先
#PriorityQueue按数据大小取最小值优先 import queue pque = queue.PriorityQueue() #优先级的队列 pque.put(7) #先存入队列 pque.put(5) pque.put(3) print(pque.qsize()) print(pque.get()) #取出最小值的数据 print(pque.get()) print(pque.get())
5.11、线程同步 --- 信号量
Lock锁是同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据,如一个场所内只有3把椅子给人坐,那么只允许3个人,其它人则排队,只有等里面的人出来后才能进去。
import threading,time class mythreading(threading.Thread): #写一个类方法继承hreading模块 def run(self): #运行线程的函数,函数名必须是run名称 semaphore.acquire() #获取信号量锁 print('running the thread:',self.getName()) time.sleep(2) semaphore.release() #释放信号量锁 if __name__ == '__main__': semaphore = threading.BoundedSemaphore(3) #创建信号量对象,只运行3个进程同时运行 for i in range(20): t1 = mythreading() t1.start() t1.join() print('---end')
import threading import time class Num: def __init__(self): self.num = 0 self.sem = threading.Semaphore(value=3) # 允许最多三个线程同时访问资源 def add(self): self.sem.acquire() # 内部计数器减1 self.num += 1 num = self.num self.sem.release() # 内部计数器加1 return num n = Num() class jdThread(threading.Thread): def __init__(self, item): threading.Thread.__init__(self) self.item = item def run(self): time.sleep(2) value = n.add() print(self.item, value) for item in range(100): t = jdThread(item) t.start() t.join()
5.12、ThreadLocal
一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题
import threading # 创建全局ThreadLocal对象: local_school = threading.local() def process_student(): # 获取当前线程关联的student: std = local_school.student print('Hello, %s (in %s)' % (std, threading.current_thread().name)) def process_thread(name): # 绑定ThreadLocal的student: local_school.student = name process_student() t1 = threading.Thread(target= process_thread, args=('dongGe',), name='Thread-A') t2 = threading.Thread(target= process_thread, args=('老王',), name='Thread-B') t1.start() t2.start() t1.join() t2.join()
说明:
全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。
可以理解为全局变量local_school是一个dict,不但可以用local_school.student,还可以绑定其他变量,如local_school.teacher等等。
ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
5.12、异步
from multiprocessing import Pool import time import os def test(): print("---进程池中的进程---pid=%d,ppid=%d--"%(os.getpid(),os.getppid())) for i in range(3): print("----%d---"%i) time.sleep(1) return "test" def test2(args): print("---callback func--pid=%d"%os.getpid()) print("---callback func--args=%s"%args) pool = Pool(3) pool.apply_async(func=test,callback=test2) time.sleep(5) print("----主进程-pid=%d----"%os.getpid())