python day 19
2019/10/31
学习资料来自老男孩教育
1. 购物商城作业要求
2. 多进程
2.1 简述多进程
程序:是一个指令的集合
进程:正在执行的程序;或者说,当你运行一个程序,你就启动了一个进程。
1. 编写完的代码,没有运行时,称为程序;正在运行的代码,称为进程
2. 程序是死的(静态的),进程是活的(动态的)
操作系统轮流让各个任务交替执行,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。
多进程中,每个进程中所有数据(包括全局变量)都各自拥有一份,互不影响。
'''
模拟多任务处理:一边唱歌一边跳舞
'''
import time
def sing():
for i in range(5):
print('唱歌')
dance()
time.sleep(0.2)
def dance():
print('跳舞')
if __name__ == '__main__':
sing()
2.2 multiprocessing模块,创建多进程程序
程序开始运行时,首先会创建一个主进程(或者叫父进程)
在主进程下,我们可以创建新的进程(子进程),子进程依赖于主进程,如果主进程结束,程序会退出。
python提供了非常好用的多进程包multiprocessing,借助这个包,可以软件构件完成从单进程到并发执行的转换。
mulprocessing模块提供了一个Process类来创建一个进程对象.
from multiprocessing import Process
import time
def sing(name):
for i in range(5):
print(''.join([name,'唱歌']))
time.sleep(1)
def dance():
for i in range(5):
print('跳舞')
time.sleep(1)
if __name__ == '__main__':
p1 = Process(target=sing,args=('lanxing',))
# target表示调用对象,args表示调用对象的位置参数元组
p2 = Process(target=dance,name='进程2')
print(p1.name)#就是进程的id,即pid
print(p2.name)
p1.start()
p2.start()
p1.join()
p2.join()
Process(target,name,args),Process类初始化时有三个属性。
Process类的常用方法:
p.start():启动进程,并调用该子进程中的p.run()
p.run():进程启动时运行的方法,正是它去调用target指定的函数,当我们自定义类时,类中一定要实现该方法。
p.terminate():强制终止进程p,不会进行任何清理操作
p.is_alive():如果p仍然运行,返回True,否则就是False。
p.join([timeout]):主进程等待p缠上,timeout是可选的超时时间。
Process类常用属性:
name:当前进程实例别名,默认为Process-N,N为从1开始递增的整数。
pid:当前进程实例的PID值
2.3 if name=='main'的说明
if name == “main”:说明
一个python的文件有两种使用的方法,第一是直接作为程序执行,第二是import到其他的python程序中被调用(模块重用)执行。
因此if name == 'main': 的作用就是控制这两种情况执行代码的过程,name 是内置变量,用于表示当前模块的名字。
在if name == 'main': 下的代码只有在文件作为程序直接执行才会被执行,而import到其他程序中是不会被执行的
在 Windows 上,子进程会自动 import 启动它的这个文件,而在 import 的时候是会执行这些语句的。如果不加if name == "main":的话就会无限递归创建子进程
所以必须把创建子进程的部分用那个 if 判断保护起来
import 的时候 name 不是 main ,就不会递归运行了
2.4 多进程中的变量
全局变量在多个进程中不共享:进程之间的数据是独立的,默认情况下互不影响。
创建新的进程还能够使用类的方式,可以自定义一个类,继承Process类,实例化这个类的时候,就等同于实例化一个进程对象。
2.5 进程池
进程池:用来创建多个进程。
当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process动态类生成多个进程,但如果是上百甚至上千个进程,手动的去创建进程的工程量巨大,此时就可以用到multiprocessing模块提供的Pool。
初始化Pool时,可以指定一个最大进程数,当有新的请求提交到Pool中时,如果池还没有满,那么就会创建一个的新的进程用来执行该请求;但如果池中的进程数已经达到指定的最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来执行。
multiprocessing.Pool常⽤函数解析:
apply_async(func[, args[, kwds]]) : 使⽤⾮堵塞⽅式调⽤func(并⾏执⾏, 堵塞⽅式必须等待上⼀个进程退出才能执⾏下⼀个进程) , args为传递给func的参数列表, kwds为传递给func的关键字参数列表;
apply(func[, args[, kwds]])(了解即可几乎不用) 使⽤阻塞⽅式调⽤func.
close(): 关闭Pool, 使其不再接受新的任务;
terminate(): 不管任务是否完成, ⽴即终⽌;(用得比较少)
join(): 主进程阻塞, 等待⼦进程的退出, 必须在close或terminate之后使⽤;
from multiprocessing import Pool
import random,time
def work(num):
print(random.random()*num)
time.sleep(3)
if __name__=='__main__':
po = Pool(3) #定义一个进程池,最大进程数为3,默认大小为CPU核数。
for i in range(10):
po.apply_async(work,(i,)) # apply_async选择要调用的目标,每次循环会用空出来的子进程去调用目标
po.close() #进程池关闭之后不再接收新的请求
po.join() #等待po中所有子进程结束,必须放在close后面。
# 在多进程中,主进程一般用来等待,真正的任务都在子进程中执行。
2.6 进程间通信Queue
多进程之间,默认是不共享数据的。
通过Queue(队列Q)可以实现进程中的数据传递。
Q本身是一个消息队列。
如何添加消息(入队操作):
from multiprocessing import Queue
q = Queue(3) # 初始化一个Queue对象,最多可接受3条消息
q.put('消息1') #添加的消息数据类型不限
q.put('消息2')
q.put('消息3')
print(q.full())
可以使用multiprocessing模块的Queue实现多进程之间的数据传递。
初始化Queue()对象时(例如:q=Queue()),若括号中没有指定最大可接收消息数或者数量为负值,那么就代表可接受的消息数量没有上限。
Queue.qsize():返回当前队列包含的消息数量
Queue.empty():如果队列为空,返回True,否则返回False
Queue.full():如果队列满了,返回True,反之False
Queue.get([block[,timeout]]):获取队列中的一条消息,然后将其从队列中移除,block默认值为True。
如果block使⽤默认值, 且没有设置timeout(单位秒) , 消息列队如果为空, 此时程序将被阻塞(停在读取状态) , 直到从消息列队读到消息为⽌,如果设置了timeout, 则会等待timeout秒, 若还没读取到任何消息, 则抛出"Queue.Empty"异常。
如果block值为False, 消息列队如果为空,则会⽴刻抛出“Queue.Empty”异常。
Queue.get_nowait():相当于Queue.get(False)
Queue.put(item,[block[,timeout]]):将item消息写入队列,block默认值为True
如果block使用默认值,且没有设置timeout(单位秒),消息队列如果已经没有空间可写入,此间程序将被阻塞(停在写入状态),直到从消息队列腾出空间为止,如果设置了Ture和timeout,则会等待timeout秒,若还没空间,则抛出Queue.Full异常。
如果block值为False,消息队列如果还没有空间可写入,则会立刻抛出Queue.Full异常。
Queue.put_nowait(item):相当于Queue.put(item,False)
进程池创建的进程之间通信:如果要使用Pool创建进程,就需要使用multiprocessing.Manager()中的Queue()而不是multiprocessing.Queue()。否则会得到一个报错信息:RuntimeError:Queue objects should only be shared between processesthrough inheritance.
3. 线程
3.1 线程介绍
线程:实现多任务的另一种形式
一个进程中,也经常需要同时做多件事,就需要同时运行多个"子任务",这些子任务,就是线程。
线程又被称为轻量级进程(lightweight process),是更小的执行单元。
一个进程可拥有多个并行(concurrent)线程,当中每一个线程,共享当前进程的资源。
一个进程中的线程共享相同的内存单元/内存地址空间-->可以访问相同的变量和对象,而且它们从同一堆中分配对象-->通信/数据交换/同步操作
由于线程间的通信是在同一地址空间上进行的,所以不需要额外的通信机制,这就使得通信更简便而且信息传递的速度也更快。
程序里面需要大量计算,就使用多进程,如果IO流多,就使用多线程。
3.2 线程和进程的区别
进程是系统进行资源分配和调度的一个独立单位。
进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
一个程序至少有一个进程,一个进程至少有一个线程。
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源,但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
线程不能够独立执行,必须依存在进程中。
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。
3.3 thread模块和threading模块
python的thread模块是比较底层的模块,在各个操作系统中表现形式不同(低级模块)。
python的threading模块是对thread做了一些包装的,可以更加方便地被使用(高级模块)。
thread有一些缺点,在threading中得到了弥补,所以直接学习threading模块。
import threading
if __name__=='__main__':
# 任何进程默认会启动一个线程,这个线程称为主线程,主线程可以启动新的子线程。
# current_thread():返回当前线程的实例
# .name:当前线程的名称
print('主线程%s启动'%(threading.current_thread().name))
import threading,time
def saySorry():
print('子线程启动%s启动'%(threading.current_thread().name))
time.sleep(1)
print('亲爱的,我错了,我能吃饭了吗?')
if __name__=='__main__':
print('主线程%s启动'%(threading.current_thread().name))
for i in range(5):
t=threading.Thread(target=saySorry) # Thread():指定线程要执行的代码
t.start()
3.4 创建线程
创建线程有两种方式:
- 第一,通过threading.Thread直接在线程中运行函数;
- 第二,通过继承threading.Thread类来创建线程
这种方法只需要重载threading.Thread类的run方法,然后调用start()开启线程就可以了。
import threading,time
class MyThread(threading.Thread):
def run(self):
for i in range(3):
time.sleep(1)
msq = ''.join(["I'm'",self.name,'@',str(i)])
# name属性中保存了当前线程的名字
print(msq)
if __name__=='__main__':
t = MyThread()
t.start()
3.5 线程的5种状态
多线程程序的执行顺序是不确定的(操作系统决定)。当执行到sleep语句时,线程将被阻塞(Blocked),到sleep结束后,线程进入就绪(Runnable)状态,等待调度。而线程调度将自行选择一个线程执行。代码中只能保证每个线程都运行完整个run函数,但是线程的启动顺序/run函数中每次循环的执行顺序都不能确定。
线程的5种状态:
- 新状态:线程对象已经创建,还没有在其上调用start()方法
- 可运行状态:当线程有资格运行,但调度程序还没有把它选定为运行线程时线程所处的状态。当start()方法调用时,线程首先进入可运行状态。在线程运行之后或者从阻塞、等待或睡眠状态回来后,也返回到可运行状态。
- 运行状态:线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。
- 等待/阻塞/睡眠状态:这是线程有资格运行时它所处的状态。实际上这三个状态组合为一种,其共同点是:线程仍旧是活的(可运行的),但是当前没有条件运行。但是如果某件事件出现,他可能返回到可运行状态。
- 死亡态:当线程的run()方法完成时就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。如果在一个死去的线程上调用start()方法,会抛出RuntimeError: threads can only be started once异常。
3.6 线程共享全局变量
在一个进程内的所有线程共享全局变量,多线程之间的数据共享(这点要比多进程好)
缺点就是:可能造成多个线程同时修改一个变量(即线程非安全),可能造成混乱。
import threading
num = 0
def test1():
global num
for i in range(1000000):
num += 1
print('线程1中',num)
def test2():
global num
for i in range(1000000):
num += 1
print('线程2中',num)
p1 = threading.Thread(target=test1)
p1.start()
p2 = threading.Thread(target=test2)
p2.start()
print('主线程中',num)
# 每次数都不同的原因是p1,p2调度时,都还未执行完。
3.7 线程同步-互斥锁
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性(原子性)。
互斥锁为资源引入一个状态:锁定/非锁定。某个线程要更改共享数据时,先将其锁定,此时资源的状态为锁定,其他线程不能更改;直到该线程释放资源,将资源的状态变成非锁定,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进入写入操作,从而保证了多线程情况下数据的正确性。
threading模块中定义了Lock类,可以方便地处理锁定。
创建锁:mutex = threading.Lock()
锁定:mutex.acquire()
锁释放:mutex.release()
import threading
num = 0
mutex = threading.Lock()
def test1():
global num
if mutex.acquire():
for i in range(1000000):
num += 1
mutex.release()
print('线程1中',num)
def test2():
global num
if mutex.acquire():
for i in range(1000000):
num += 1
mutex.release()
print('线程2中',num)
p1 = threading.Thread(target=test1)
p1.start()
p2 = threading.Thread(target=test2)
p2.start()
print('主线程中',num)
3.8 线程同步-死锁
死锁(错误情况,理解即可):在线程间共享多个资源的时候,如果两个线程分别占有⼀部分资源并且同时等待对⽅的资源, 就会造成死锁 。
⼊到了死锁状态, 可以使⽤ctrl-z退出。
import threading
import time
class MyThread1(threading.Thread):
def run(self):
if mutexA.acquire():
print(self.name + '---do1---up---')
time.sleep(1)
if mutexB.acquire():
print(self.name + '---do1---down---')
mutexB.release()
mutexA.release()
class MyThread2(threading.Thread):
def run(self):
time.sleep(1)
if mutexB.acquire():
print(self.name + '---do2---up---')
if mutexA.acquire():
print(self.name + '---do2---down---')
mutexA.release()
mutexB.release()
if __name__ == '__main__':
mutexA = threading.Lock()
mutexB = threading.Lock()
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
在多线程程序中,死锁问题很大一部分是由于线程同时获取多个锁造成的。如一个线程获取了第一个锁,然后在获取第二个锁的时候发生阻塞,那么这个线程就可能阻塞其他线程的执行,从而导致整个程序假死(两个人每人一根筷子)
3.9 同步和异步
同步调⽤:确定调用的顺序
按顺序购买四大名著
异步调⽤:不确定顺序
你喊你朋友吃饭 , 你朋友说知道了 , 待会忙完去找你 ,你就去做别的了
堵塞和非堵塞
# 多个线程有序执⾏
import threading,time
class Task1(threading.Thread):
def run(self):
while True:
if lock1.acquire():
print('-----Task1-----')
time.sleep(1)
lock2.release()
class Task2(threading.Thread):
def run(self):
while True:
if lock2.acquire():
print('-----Task2-----')
time.sleep(1)
lock3.release()
class Task3(threading.Thread):
def run(self):
while True:
if lock3.acquire():
print('-----Task3-----')
time.sleep(1)
lock1.release()
lock1 = threading.Lock()
#创建另外一把锁,并且锁上
lock2 = threading.Lock()
lock2.acquire()
#再创建一把锁并锁上
lock3 = threading.Lock()
lock3.acquire()
t1 = Task1()
t2 = Task2()
t3 = Task3()
t1.start() t2.start() t3.start()
# 无序执行,方式很多
import threading,time
num = 0
def test1():
global num
mutex.acquire()
for i in range(1000000):
num += 1
mutex.release()
print("1",num)
def test2():
global num
while True:
if mutex.acquire(False):
for i in range(1000000):
num += 1
print("2", num)
mutex.release()
break
else:
print("该干嘛干嘛")
mutex = threading.Lock()
p1 = threading.Thread(target=test1)
p2 = threading.Thread(target=test2)
p1.start()
p2.start()
3.10 生产者消费者模式
⽣产者消费者模式:
在线程世界⾥, ⽣产者就是⽣产数据的线程, 消费者就是消费数据的线程(做包子,吃包子)。
经常会出现生产数据的速度大于消费数据的速度,或者生产速度跟不上消费速度。
⽣产者消费者模式是通过⼀个容器(缓冲区)来解决⽣产者和消费者的强耦合问题(耦合指关系密切)。
例如两个线程共同操作一个列表,一个放数据,一个取数据。
⽣产者和消费者彼此之间不直接通讯, ⽽通过阻塞队列来进⾏通讯。
Python的Queue模块:实现了3种类型的队列来实现线程同步,包括:
FIFO(先⼊先出) 队列 Queue,
LIFO(后⼊先出) 栈 LifoQueue,
优先级队列 PriorityQueue
区别在于队列中条目检索的顺序不同:
在FIFO队列中,按照先进先出的顺序检索条目。
在LIFO队列中,最后添加的条目最先检索到(操作类似一个栈)。
在优先级队列中,条目被保存为有序的(使用heapq模块)并且最小值的条目被最先检索。
这些队列都实现了锁原语(可以理解为原⼦操作, 即要么不做, 要么就做完) , 能够在多线程中直接使⽤。
现阶段只要求掌握其中一种,FIFO队列。
class queue.Queue(maxsize=0):
FIFO队列的构造器。maxsize为一个整数,表示队列的最大条目数,可用来限制内存的使用。
一旦队列满,插入将被阻塞直到队列中存在空闲空间。如果maxsize小于等于0,队列大小为无限。maxsize默认为0。
import threading
import time
from queue import Queue
class Pro(threading.Thread):
def run(self):
global queue
count = 0
while True:
if queue.qsize()<1000:
for i in range(100):
count = count + 1
msg = '生成产品' + str(count)
queue.put(msg)#队列中添加新产品
print(msg)
time.sleep(1)
class Con(threading.Thread):
def run(self):
global queue
while True:
if queue.qsize() > 100:
for i in range(3):
msg = self.name + '消费了' + queue.get()
print(msg)
time.sleep(1)
if __name__ == "__main__":
queue = Queue()
#创建一个队列。线程中能用,进程中不能使用
for i in range(500):#创建500个产品放到队列里
queue.put(‘初始产品’ + str(i))#字符串放进队列
for i in range(2):#创建了两个线程
p = Pro()
p.start()
for i in range(5):#5个线程
c = Con()
c.start()
3.11 ThreadLocal变量
⼀个ThreadLocal变量虽然是全局变量, 但每个线程都只能读写⾃⼰线程的独⽴副本, 互不⼲扰。ThreadLocal解决了参数在⼀个线程中各个函数之间互相传递的问题。
可以理解为全局变量local_school是⼀个dict, 可以绑定其他变量。
ThreadLocal最常⽤的地⽅就是为每个线程绑定⼀个数据库连接, HTTP请求, ⽤户身份信息等, 这样⼀个线程的所有调⽤到的处理函数都可以⾮常⽅便地访问这些资源。
import threading
#创建一个全局的对象
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=('zhangsan',),name='t1')
t2 = threading.Thread(target=process_thread,args=('老王',),name='t2')
t1.start()
t2.start()