文章目录
一、进程
1、定义
进程:由操作系统控制,创建并管理(创建、启动、挂起和销毁),每个进程占用独立的资源
并发:伪并行,就是几个进程共用一个cpu,几个进程之间是时间轮换的,而这个轮换时间很短,肉眼是看不到的,所以我们肉眼看到的是几个进程同时在进行,其他中间有停止的,只是停止时间短,这就叫并发
并行:指一个进程就用一个cpu,每个进程是没有停止的,就是一起运行的
同步:一个任务执行完后,另一个任务才能开始执行,就是有一个任务需要等待,这样两个任务之间的关系是同根生的,非常紧密的
异步:两个任务之间没有关联,你执行你的,我执行我的,互相不用等待
2、进程实现
from multiprocessing import Process
class MyProccess(Process):
def __init__(self):
super().__init__()
def run(self):
print('hello')
if __name__ == '__main__':
p1=MyProccess()
p1.start() # 会调用MyProccess的run方法
--------结果如下-----------
hello
3、应用
import os
import time
from multiprocessing import Process
def music():
time.sleep(2)
print('music正在占用的pid为', os.getpid())
print("听音乐。。。")
def lol():
time.sleep(2)
print('lol正在占用的pid为', os.getpid())
print('打游戏。。。')
if __name__ == '__main__':
print('主进程的pid为',os.getpid()) # 获取当前线程的pid
p1 = Process(target=music)
p2 = Process(target=lol)
p1.daemon = True # 守护进程,默认值是False,当为True的时候主进程结束则守护进程也强制结束
p2.daemon = True
p1.start()
p2.start()
p1.join() # 线程阻塞,上面的代码任务没有执行完,不会运行下面的代码
p2.join()
print('主进程结束')
--------结果如下-----------
主进程的pid为 33912
music正在占用的pid为 33914
听音乐。。。
lol正在占用的pid为 33915
打游戏。。。
主进程结束
注意:
1)守护进程daemon默认值是False,设置为True后主进程结束则守护进程也强制结束,该设置必须写在start()前面;
2)加入代码p1.join() 后,join后面的主进程程序必须等子进程执行完后在执行
4、同步锁
import time
from multiprocessing import Process, Lock
def fun(name):
with open('water.txt', 'r', encoding='utf-8') as f:
n = f.read()
print(f"{name}看到剩余{n}瓶")
def buy_water(name, lock):
time.sleep(0.5)
lock.acquire()
with open('water.txt', 'r', encoding='utf-8') as fp:
n = fp.read()
if n == '1':
print(f'{name}买到水了!')
n = int(n) - 1
elif n == '0':
print(f'很遗憾,{name}没买到水!')
with open('water.txt', 'w', encoding='utf-8') as f:
f.write(str(n))
lock.release()
if __name__ == '__main__':
lock = Lock()
for i in range(10):
fun(i)
p = Process(target=buy_water, args=(i, lock))
p.start()
--------结果如下-----------
0看到剩余1瓶
1看到剩余1瓶
2看到剩余1瓶
3看到剩余1瓶
4看到剩余1瓶
5看到剩余1瓶
6看到剩余1瓶
7看到剩余1瓶
8看到剩余1瓶
9看到剩余1瓶
5买到水了!
很遗憾,6没买到水!
很遗憾,0没买到水!
很遗憾,1没买到水!
很遗憾,9没买到水!
很遗憾,2没买到水!
很遗憾,3没买到水!
很遗憾,7没买到水!
很遗憾,8没买到水!
很遗憾,4没买到水!
以上示例可以看出:同步锁把抢水的程序锁起来了,所以,虽然有10个用户都在抢票,但实际上只有一人能进入抢水程序,其他的人要等待第一个进抢水程序的人执行完后才能又进一个人,反正每次就只允许一个人在使用抢水程序,其他的人继续排队
5、信号量
Semaphore的内部维护了一个计数器,每一次acquire操作都会让计数器减1,每一次release操作都会让计数器加1,当计数器为0时,任何线程的acquire操作都不会成功,Semaphore确保对资源的访问有一个上限, 这样,就可以控制并发量
import time
from multiprocessing import Process, Semaphore
def play_game(name, s):
s.acquire()
time.sleep(0.5)
print(f"{name}在打游戏")
time.sleep(1)
print(f"{name}打完了")
time.sleep(1)
s.release()
if __name__ == '__main__':
s = Semaphore(4)
for name in range(10):
p = Process(target=play_game, args=(name, s))
p.start()
--------结果如下-----------
3在打游戏
0在打游戏
2在打游戏
1在打游戏
2打完了
1打完了
0打完了
3打完了
4在打游戏
6在打游戏
5在打游戏
7在打游戏
5打完了
6打完了
4打完了
7打完了
8在打游戏
9在打游戏
8打完了
9打完了
以上示例可以看出:信号量就是对一段程序设定允许最多几个人使用,相当于一个游戏厅,只有四个位置,最多允许四个人在游戏,后面人要等待前面的人打完才能打。
6、事件
事件event中有一个全局内置标志Flag,值为 True 或者False。使用wait()函数的线程会处于阻塞状态,此时Flag指为False,直到有其他线程调用set()函数让全局标志Flag置为True,其阻塞的线程立刻恢复运行,还可以用isSet()函数检查当前的Flag状态
import time
from multiprocessing import Process, Event
def test(e):
print("子进程开始")
time.sleep(2)
print("子进程结束")
# 设置事件值为Ture,我们还可以通过e.clear()把值改为False
e.set()
if __name__ == '__main__':
# 创建事件对象e,此时默认值为False
e = Event()
# 创建子进程,把e传给子进程
p = Process(target=test, args=(e,))
p.start()
print('等待子进程结束后拿到值')
# 当值为False会阻塞,当值为Ture的时候,不会阻塞,因为我们在子进程中最好把值改为Ture,所以wait()以后的程序要等到子进程执行完才能执行
e.wait()
print('拿到值,执行主进程')
time.sleep(1)
print('主进程执行完毕')
--------结果如下-----------
子进程开始
等待子进程结束后拿到值
子进程结束
拿到值,执行主进程
主进程执行完毕
总结:
注意互斥锁Lock与事件Event区别,需求不同,使用方式也不同:
-
互斥锁Lock主要针对多个线程同时操作同一个数据,使用互斥锁可以保证数据正常修改或者访问;
-
事件Event主要用于唤醒正在阻塞等待状态的线程/进程;
7、队列,消费者生产者模型
1、队列
队列就相当于一个容器,里面可以放数据,特点是先放进去先拿出来,即先进先出
#coding:utf-8
from multiprocessing import Process,Queue
def work01(q:Queue):
print('work01正在工作。。。')
while not q.empty():
print(f'work01获得了{q.get()}')
q.task_done() # 这个任务做完了
def work02(q:Queue):
print('work02正在工作...')
while not q.empty():
print(f'work02获得了{q.get()}')
q.task_done() # 这个任务做完了
if __name__ == '__main__':
q=Queue()
q.put('a')
q.put('b')
q.put('c')
p1=Process(target=work01,args=(q,))
p2=Process(target=work02,args=(q,))
p1.start()
p2.start()
print('主进程运行完毕')
# 使用list: work01 work02会做重复工作 p1 p2 进程 独享 list内存
# 使用Queue: work01 work02共同完成q中的所有任务 Queue 是被 p1 p2 共享的
from multiprocessing import Queue
q=Queue(2) #创建一个队列对象,并给他设置容器大小,即能放几个数据
q.put(1) #put()方法是往容器里放数据
q.put(2)
q.put(3) #这是往容器里放第三个数据,由于只能放两个,所以此处会阻塞,不会报错,相当于死循环
q.put_nowait(4) #put_nowait()这也是从容器里放数据的方法,但如果容器满了,不会阻塞,会直接报错
q.get() #get()方法是从容器里拿数据
q.get()
q.get() #这是从容器里拿第三个数据,但是容器的两个数据拿完了,没有数据了,此时也会阻塞,不会报错,死循环
q.get(False) #这也是从容器里拿数据的方法,当没数据时不会阻塞,直接报错
q.get_nowait() #这也是从容器里拿数据的方法,当没数据时不会阻塞,直接报错
q.full() #这是查看容器是否满了,如果满了,返回Ture,否则返回False
q.empty() #这是查看容器是否为空,如果为空,返回Ture,否则返回False
2、消费者生产者模型
就是消费者和生产者之间不是直接联系的,而是通过中间的一个队列来进行联系的,生产者把生产的东西放进进队列里,消费者从队列里拿东西,其实消费者和生产者之间没有实质的联系
import time
from multiprocessing import Process, Event, Queue
def product(q: Queue):
for i in range(10):
time.sleep(1)
q.put(i)
print(f"零件{i}已产出")
q.put('over')
def customs(q: Queue):
while True:
time.sleep(1)
s = q.get()
if s == 'over':
break
print(f"零件{s}已打包")
if __name__ == '__main__':
q = Queue(10)
p1 = Process(target=product, args=(q,))
p2 = Process(target=customs, args=(q,))
p1.start()
p2.start()
--------结果如下-----------
零件0已产出
零件0已打包
零件1已产出
零件1已打包
零件2已产出
零件2已打包
零件3已产出
零件3已打包
零件4已产出
零件4已打包
零件5已产出
零件5已打包
零件6已产出
零件6已打包
零件7已产出
零件7已打包
零件8已产出
零件8已打包
零件9已产出
零件9已打包
以上示例虽然实现了生产者和消费者的功能,但是存在一个问题就是:有个问题就是,生产者要把生产结束的消息放进队列里,让消费者过去结束消息,消费者才知道队列里没有数据了,这样消费者才能停止,如果生产者不放结束消息,当生产者结束时,即不往队列放数据,而消费者不知道生产者已经结束,还一直往队列拿数据,当队列没数据时,消费者一边就会阻塞。解决阻塞,有几个消费者,生产者就应该放几个结束消息,让每个消费者都知道,这样每个消费者才能结束。但是生产者又不知道有几个消费者,所以不知奥应该放几个结束数据,这样就无法解决对消费者现象。此时,我们就就可以引入joinablequeue
3、joinablequeue
import time
from multiprocessing import Process, Queue, JoinableQueue
def product(q: Queue):
for i in range(10):
time.sleep(1)
q.put(i)
print(f"零件{i}已产出")
# 当q收到的task_done数量等于放进q的数据数量时,生产者就结束了
q.join()
def customs(q: Queue):
while True:
time.sleep(1)
s = q.get()
if s == 'over':
break
print(f"零件{s}已打包")
q.task_done()
if __name__ == '__main__':
q = JoinableQueue()
p1 = Process(target=product, args=(q,))
p1.start()
for i in range(3):
p2 = Process(target=customs, args=(q,))
# 把创建的每个消费者子进程设为守护进程
p2.daemon = True
p2.start()
# 主进程要等到生产者子进程结束后才结束
p1.join()
--------结果如下-----------
零件0已产出
零件0已打包
零件1已产出
零件1已打包
零件2已产出
零件2已打包
零件3已产出
零件3已打包
零件4已产出零件4已打包
零件5已产出零件5已打包
零件6已产出
零件6已打包
零件7已产出
零件7已打包
零件8已产出零件8已打包
零件9已产出
零件9已打包
整个个过程就是:生产者生产了10个零件,3个消费者打包零件,每打包一个零件往q里发一个task_done,当q拥有10个task_done时,意味着10个零件打包完了,此时,生产者就结束了,接着主进程也也结束了,然后守护进程跟着结束了,即所有的消费者子进程结束,解决上面所遇到的问题
8、进程池
定义一个池子,在里面放上固定数量的进程,有需求来了,就拿一个池中的进程来处理任务,等到处理完毕,进程并不关闭,而是将进程再放回进程池中继续等待任务。如果有很多任务需要执行,池中的进程数量不够,任务就要等待之前的进程执行任务完毕归来,拿到空闲进程才能继续执行。也就是说,池中进程的数量是固定的,那么同一时间最多有固定数量的进程在运行。这样不会增加操作系统的调度难度,还节省了开闭进程的时间,也一定程度上能够实现并发效果
def square(x):
return x * x
if __name__ == '__main__':
pool = Pool(2)
result=pool.map(square,[1,2,3,4,5]) # map 会阻塞主程序
print(result)
pool.close()
print("主进程结束")
--------结果如下-----------
[1, 4, 9, 16, 25]
主进程结束
1、进程池同步方法
就是把进程搞成同步的,只有一个执行完了,下一个进程才能执行,主要运用apply()方法
from multiprocessing import Process, Pool
def test(i):
sum = 0
for n in range(5):
sum += i
return sum
if __name__ == '__main__':
p = Pool(4)
for i in range(10):
# 调用进程池的apply()方法,虽是10个进程,但是只能一个接着一个执行,此时返回的一个值
p1 = p.apply(func=test, args=(i,))
print(p1)
--------结果如下-----------
0
5
10
15
20
25
30
35
40
45
2、进程池异步方法
运用apply_async()方法
from multiprocessing import Process, Pool
def test(i):
sum = 0
for n in range(5):
sum += i
return sum
if __name__ == '__main__':
p = Pool(4)
l1 = []
for i in range(10):
# 调用进程池的apply_async()方法,由于进程池只能允许有4个进程,所以,相当于同时有4个进程在进程池执行,但现在返回的不是值,而是一个对象
p1 = p.apply_async(func=test, args=(i,))
# 我们把对象放在一个列表,等所有程序结束再从列表拿值,如现在就用res.get()去拿值,就会形成阻塞,从而演变成进程一个接一个执行,变成同步执行的
l1.append(p1)
p.close()
p.join()
for j in l1:
print(j.get())
--------结果如下-----------
0
5
10
15
20
25
30
35
40
45
3、回调函数
import time
from multiprocessing import Process, Pool
def test(num):
return num
def call_test(i):
print("我是回调函数")
if __name__ == '__main__':
p = Pool(4)
p.apply_async(func=test, args=(5,), callback=call_test)
time.sleep(2)
注意:一个问题是,若不在主进程中写入一个延迟2秒,当主进程代码走完,而子进程还没执行完,从而子进程会随着主进程结束而结束,从而回调函数也死亡
二、线程
1、定义
线程:在进程中创建,线程不拥有资源,共享进程的资源
2、应用
import threading
import time
def music(user):
print(f'{user}正在听音乐....')
print(f'{threading.current_thread().name}正在运行...')
time.sleep(5)
print(f'{threading.current_thread().name}运行即将结束。')
def lol(user):
time.sleep(8)
if __name__ == '__main__':
t1 = threading.Thread(target=music, args=('无极',), name='线程1')
t2 = threading.Thread(target=lol, args=('无极',), name='线程2')
t1.start()
t2.start()
t1.join() # 阻塞主程序,但不会阻塞线程2
t2.join()
print('主程序结束!')
3、线程锁
import threading
n = 200000
lock = threading.Lock()
print(hasattr(lock, '__enter__')) # True
print(hasattr(lock, '__exit__')) # True
def work():
global n
for i in range(1000000):
# 上锁
lock.acquire()
n -= 1
# 释放锁
lock.release()
# with lock:
# n -= 1
if __name__ == '__main__':
# 两个线程共同操作n
t1 = threading.Thread(target=work)
t2 = threading.Thread(target=work)
t1.start()
t2.start()
t1.join()
t2.join()
print(n)
实例:
“”"
一个列表中有100个url地址(假设请求每个地址需要0.5秒),请设计程序一个程序,获取列表中的url地址,使用4个线程去发送这100个请求,计算出总耗时!
“”"
"""
一个列表中有100个url地址(假设请求每个地址需要0.5秒),请设计程序一个程序,获取列表中的url地址,使用4个线程去发送这100个请求,计算出总耗时!
"""
import time
from multiprocessing.pool import ThreadPool
import queue
def download(q: queue.Queue):
while not q.empty():
print(q.get())
time.sleep(0.5)
q.task_done() # 这个任务做完了
def calc_time(func):
def wrap(*args, **kwargs):
start = time.time()
func(*args, **kwargs)
end = time.time()
print(f"共花费时间{(end - start):.2f}秒")
return wrap
@calc_time
def main():
q = queue.Queue()
for i in range(100):
q.put(f'http://www.gushi.com/page={i}')
pool = ThreadPool(4) # 4个线程
pool.apply_async(download, args=(q,)) # pool中的多个线程异步地调用指定函数 这里是 download
q.join() # 等待所有的任务做完
print('finished!')
if __name__ == '__main__':
main()
注意:
Queue.task_done() 与Queue.join()配合使用,在完成一项工作之后,会向任务已经完成的队列发送一个信号,Queue.join() 实际上意味着等到队列为空,再执行其他操作;
如果线程里每从队列里取一次,但没有执行task_done(),则join无法判断队列到底有没有结束,在最后执行join()是等不到信号结果的,会一直挂起。即每task_done一次 就从队列里删掉一个元素,这样join的时候根据队列长度是否为零来判断队列是否结束,从而执行其他操作。
三、进程与线程的区别
区别:
1)资源(主要指内存资源):进程有,线程没有
2)切换:线程比进程快
3)一个程序运行,至少一个进程,至少一个线程
使用场景:
1)CPU密集的不适合多线程,一直占用CPU的场景不适合多线程(工程机器不停地需要工人操作,不适合一个人操作多台机器)
2)间歇性占用CPU的场景适合多线程
四、协程
1、定义
协程,又称微线程,线程,也称为用户级线程,在不开辟线程的基础上完成多任务,也就是在单线程的情况下完成多任务,多个任务按照一定顺序交替执行 通俗理解只要在def里面只看到一个yield关键字表示就是协程
满足协程的条件:
1)挂起当前状态(暂停)yield
2)激活挂起的状态(恢复)send
2、生成器实现并发
import time
def work01():
for i in range(5):
print("work01...")
time.sleep(1)
yield
def work02():
for i in range(5):
print("work02...")
time.sleep(1)
yield
if __name__ == '__main__':
w1 = work01()
w2 = work02()
while True:
try:
next(w1)
next(w2)
except:
break
3、gevent实现协程
gevent内部封装了greenlet,其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。
import time
from gevent import monkey
monkey.patch_all()
import gevent
def work1():
for i in range(5):
print('work1:听音乐...')
time.sleep(1)
def work2():
for i in range(5):
print('work2:打游戏...')
time.sleep(1)
def main():
g1 = gevent.spawn(work1) # 创建协程1
g2 = gevent.spawn(work2) # 创建协程2
g1.join()
g2.join()
print('所有任务执行完毕!')
if __name__ == '__main__':
start = time.time()
main()
end = time.time()
print(f'总共花费{(end - start):.2f}秒。')
4、asyncio实现协程
import time
import asyncio # 异步IO库, 单线程实现并发 —— 协程
# async 关键字 await关键字
async def work1(): # aysnc 任务work1是异步的
for i in range(5):
print(f'work1:听音乐...')
await asyncio.sleep(1)
async def work2():
for i in range(5):
print(f'work2:打游戏...')
await asyncio.sleep(1)
async def main():
task1 = asyncio.create_task(work1()) # 任务一
task2 = asyncio.create_task(work2()) # 任务二
await task1
await task2
if __name__ == '__main__':
start = time.time()
asyncio.run(main())
end = time.time()
print(f'共花费时间{(end - start):.2f}秒')