并发编程总结(2)
一、 信号量
- 信号量其实也是一把锁。
- 互斥锁只能让一个线程使用,而信号量同一时间可以让多个线程使用。在实例化一个信号量时,传的参数限定了可以给几个线程同时使用
- 需要导一个
from threading import Semaphore
实例:
from threading import Semaphore, Lock
from threading import current_thread
from threading import Thread
import time
sm = Semaphore(5) #
mutex = Lock() #
def task():
# mutex.acquire()
sm.acquire()
print(f'{current_thread().name}执行任务')
time.sleep(1)
sm.release()
# mutex.release()
for line in range(20):
t = Thread(target=task)
t.start()
# 结果:
Thread-1执行任务
Thread-2执行任务
Thread-3执行任务
Thread-4执行任务
Thread-5执行任务
此处等待1秒 # 就是每个线程里的那个 time.sleep(1)
Thread-6执行任务
Thread-9执行任务
Thread-7执行任务
Thread-8执行任务
Thread-10执行任务
此处等待1秒
Thread-11执行任务
Thread-14执行任务
Thread-12执行任务
Thread-13执行任务
Thread-15执行任务
此处等待1秒
Thread-16执行任务
Thread-18执行任务
Thread-17执行任务
Thread-19执行任务
Thread-20执行任务
此处等待1秒
二、 Event
1. 什么是Event
- 它是threading 中的一个方法。用来控制线程的阻塞和取消阻塞。
2. Event 有什么用
- 用来控制线程的执行,由一些线程来控制另一些线程的执行。
3. Event 的方法
wait
当一个线程中存在这样一个代码时,遇到它就会进入永久阻塞态。set
当一个线程中存在这样的代码时,遇到它会把所有的wait
打开。这样原来的线程进入就绪态。
实例:
def light():
print('红灯亮...')
time.sleep(5)
# 应该开始发送信号,告诉其他线程准备执行
e.set() # 将car中的False ---> True
print('绿灯亮...')
def car(name):
print('正在等红灯....')
# 让所有汽车任务进入阻塞态
e.wait() # False
print(f'{name}正在加速漂移....')
# 让一个light线程任务 控制多个car线程任务
t = Thread(target=light)
t.start()
for line in range(10):
t = Thread(target=car, args=(f'童子军jason{line}号', ))
t.start()
三、 线程池与进程池
1. 什么是线程池与进程池
- 进程池与线程池是用来控制当前程序允许创建的进程/线程 的数量。
2. 进程池与线程池有什么用
- 保证在硬件允许的范围内创建进程/线程的数量
3. 使用方法
导库:
from concurrent.futures import ProcessPoolExecutor
实例化一个进程池/线程池对象:
pool = ThreadPoolExecutor(n)
, n可填可不填,填了表示开启n个进程/线程 , 不填表示 默认以CPU的个数限制进程数,默认以CPU个数 * 5 限制线程数submit
传的参数为一个函数的地址(即函数名),作用是异步提交任务。submit(任务函数地址).add_done_callback(回调函数的地址)
。当任务函数有返回值时,通过回调函数接收。回调函数的形参如res
,res
还不是任务函数的返回值。通过res.result()
得到任务函数的返回值。shutdown
它会让所有线程池任务结束后,才往下执行代码,类似进程/线程中join
的作用
实例:
# 任务函数没有返回值时*************************
def task():
print('线程任务开始了...')
time.sleep(3)
print('线程任务结束了...')
for line in range(5):
pool.submit(task)
pool.shutdown()
print('hello')
# 任务函数有返回值时**********************************
def task(res):
# res == 1
print('线程任务开始了...')
time.sleep(1)
print('线程任务结束了...')
return 123
# 回调函数
def call_back(res):
print(type(res))
# 注意: 赋值操作不要与接收的res同名
res2 = res.result()
print(res2)
for line in range(5):
pool.submit(task, 1).add_done_callback(call_back)
pool.shutdown()
print('hello')
四、协程(只使用一个线程)
1. 进程/线程/协程的区别
- 进程:资源单位
- 线程:执行单位
- 协程:在单线程下实现并发
- 注意:协程不是操作系统中的一个概念,它是人为产生的一个名字,为了让单线程实现并发。
2. 协程的作用
- 多道技术的核心是:切换 + 保存状态
- 一个内存中同时放入多道任务
- 多道任务在CPU上轮流执行,遇到IO或者占用CPU时间过长就切换
- 切换时会保存作业进度
协程
通过手动模拟操作系统 “多道技术” 实现 切换 + 保存状态
让线程当做CPU , 多个任务 当做多进程/线程。
- 就是让一个线程在多个任务之间来回切换 + 保存状态
- 对于IO密集型中,遇到IO就切换 + 保存状态
对于计算密集型中,来回切换 + 保存状态。对于计算密集型,使用协程效率会更低。
优点:
在IO密集型的情况下,会提高效率
缺点:
在计算密集型的情况下,因为来回切换,效率更低
3. 如何实现协程
(1)计算密集型情况
- 用
yield
来保存状态 - 用并发来实现切换
实例:
import time
def func1():
while True:
10000000+1
yield
def func2():
g = func1() # 启动生成器 ,g 为生成器对象
for i in range(10000000):
time.sleep(10) # 模拟IO,yield并不会捕捉到并自动切换
i+1
next(g)
start = time.time()
func2()
stop = time.time()
print(stop-start)
(2)IO密集型情况
要导入一个第三方库:
from gevent import monkey,spawn , joinall
gevent
是一个第三方库,可以帮我们监听IO操作并切换使用
gevent
的目的是为了实现单线程下,遇到IO, 保存状态 + 切换monkey.patch_all()
可以监听程序下所有IO操作spawn 和 joinall
可以实现 保存状态 + 切换
实例:
from gevent import monkey
monkey.patch_all() # 可以监听该程序下所有的IO操作
import time
from gevent import spawn, joinall # 用于做切换 + 保存状态
def func1():
print('1')
# IO操作
time.sleep(1)
def func2():
print('2')
time.sleep(3)
def func3():
print('3')
time.sleep(5)
start_time = time.time()
s1 = spawn(func1) # 传任务
s2 = spawn(func2)
s3 = spawn(func3)
# s2.join() # 发送信号,相当于等待自己 (在单线程的情况下)
# s1.join()
# s3.join()
# 必须传序列类型,如列表和元组
joinall([s1, s2, s3])
end_time = time.time()
print(end_time - start_time)
五、单线程下实现服务端并发
实例:
# 客户端*************************************
import socket
import time
from threading import Thread, current_thread
def client():
client = socket.socket()
client.connect(
('127.0.0.1', 9527)
)
print('启动客户端...')
number = 0
while True:
send_data = f'{current_thread().name} {number}'
client.send(send_data.encode('utf-8'))
data = client.recv(1024)
print(data.decode('utf-8'))
number += 1
# 模拟了300个用户并发去访问服务端
for i in range(300):
t = Thread(target=client)
t.start()
# 服务端*********************************
from gevent import monkey
monkey.patch_all() # 检测IO
import socket
import time
from threading import Thread
from gevent import spawn
server = socket.socket()
server.bind(
('127.0.0.1', 9527)
)
server.listen(5)
print('启动服务端...')
# 线程任务,执行接收客户端消息与发送消息给客户端
def working(conn):
while True:
try:
data = conn.recv(1024).decode('utf-8')
if len(data) == 0:
break
print(data)
# time.sleep(1)
send_data = data.upper().encode('utf-8')
conn.send(send_data)
except Exception as e:
print(e)
break
conn.close()
def server2():
while True:
conn, addr = server.accept()
# print(addr)
# t = Thread(target=working, args=(conn,))
# t.start()
spawn(working, conn)
if __name__ == '__main__':
s1 = spawn(server2)
s1.join()
六、 并发编程中遇到的模块
1. 进程中需要导入的模块和方法
from multiprocessing import Process , Lock , RLock , Queue , Semaphore , Event
# 分别用来:创建进程,创建互斥锁,创建递归锁,创建队列,创建信号量 ,创建事件
2. 线程中需要导入的模块和方法
from threading import Thread , current_thread , Lock , RLock , queue , Semaphpre , Event
# 分别用来:创建线程,返回当前线程的信息 , 创建互斥锁,创建递归锁,创建队列,创建信号量 ,创建事件
3. 进程池和线程池中需要导入的模块和方法
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
# 分别用来: 创建进程池 , 创建线程池
4. 协程中需要导入的模块和方法
from gevent import monkey , spawn , joinall
# 分别用来 监听线程中的所有IO操作 , 调用定义的任务 , 让主线程等待所有调用的任务执行结束再结束