No.35池
No.35
今日概要
- 锁
- 互斥锁
- 递归锁
- queue 模块
- 先进先出
- 后进先出
- 优先级队列
- 池
- 进程池
- 线程池
内容回顾
-
进程
- IPC机制
- Queue
- 生产者消费者模型
- JoinableQueue
- 生产者消费者模型
- Queue
- IPC机制
-
什么是生产者消费者模型?
- 把生产数据和处理数据的过程解耦
- 让生产数据和处理数据达到工作效率平衡
- 中间的容器
- 在多进程中通过
Queue或者JoinableQueue来控制数据量- 当数据过剩时,队列的大小会控制生产者的行为。
- 当数据不足时,对列的大小会控制消费者的行为。
- 我们还可以通过定期检查队列中的元素个数,来调节生产者和消费者的个数。
- 在多进程中通过
- 案例
- 爬虫
- 假设请求网页的平均时间为0.3s,处理网页代码的平均时间为0.003s。
- 请求网页和处理网页的时间效率差为100倍,那么可以通过生产者消费者模型把整个过程解耦。
- 每启动100个线程生产数据,就对应启动1个线程处理数据。
- server
- 假设每秒有6w条请求,而一个服务每秒只能处理2k条请求。
- 先写一个web程序,只负责接收请求然后把请求放入队列中。
- 再写很多个server端,分别从队列中获取请求进行处理然后返回结果。
- 爬虫
-
线程
- 开销小、数据共享、是进程的一部分不能独立存在。
- GIL锁
- 全局解释器锁
- cpython解释器中的机制
- 导致了在同一进程中的多个线程不能实现并行
-
threading模块
-
创建线程的方式
-
面向函数
from threading import Thread def func(arg): print(arg) t = Thread(target=func, args=(666,)) t.start -
面向对象
from threading import Thread class MyThread(Thread): def __init__(self, arg): self.arg = arg super().__init__() def run(self): print(self.arg) t = MyThread(666) t.start()
-
-
线程中的方法:
- 线程对象
- t.start()
- t.join()
- 守护线程
- t.daemon = True
- 等待所有的非守护子线程都结束之后才会结束
- 函数
- current_thread()
- 在哪个线程中被调用,就返回当前线程的对象
- enumerate
- 返回当前活着的线程对象列表
- active_count
- 返回当前活着的线程对象个数,相当于
len(enumerate())。
- 返回当前活着的线程对象个数,相当于
- current_thread()
- 线程对象
-
-
总结
-
操作系统
- 多道:遇到IO会切换
- 分时:时间片到了会切换
-
进程特点:
-
数据隔离
-
IPC机制
-
开销大
-
能并行
-
进程之间共享数据
-
Manager类
-
Lock锁
-
-
-
生产者消费者模型
-
-
生成器
g1 = filter(lambda n: n % 2 == 0, range(10)) # g1 = [0 2 4 6 8] g2 = map(lambda n: n * 2, range(3)) # g2 = [0 2 4] for i in g1: # i=0 # i=1 for j in g2: # 0 2 4 # 此时g2已经为空,for循环不执行。 print(i * j)def multipliers(): return [lambda x:i*x for i in range(4)] # 列表生成式,存放了4个函数[lambda x:3x,...] print([m(2) for m in multipliers()]) # [6, 6, 6, 6]
内容详细
1.锁
from threading import Thread
a = 0
def add():
global a
for i in range(500000):
a += 1
def sub():
global a
for i in range(500000):
a -= 1
t1 = Thread(target=add)
t1.start()
t2 = Thread(target=sub)
t2.start()
t1.join()
t2.join()
print(a)
# 结果是随机的
# 对于cpython解释器,即便线程有GIL锁,也会出现数据不安全问题。
a = 0
def add():
global a
a += 1
import dis
dis.dis(add)
# add函数对应cpu中的执行指令
0 LOAD_GLOBAL 0 (a)
2 LOAD_CONST 1 (1)
4 INPLACE_ADD
6 STORE_GLOBAL 0 (a) '存回结果'
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
# 线程1执行:获取全局变量a的值,在cpu中做加法运算(a=0+1),还未将结果存入回去就轮转到线程2执行。
# 线程2执行:获取全局变量a的值依旧为0,并未获取到线程1执行后的结果,这样就导致了数据不安全问题。
多线程数据不安全问题:
- 操作的是全局变量
- 涉及到先计算再赋值的操作
+= 、-= 、*= 、/=- 包括
lst[0] += 1、dic['key'] -= 1
from threading import Thread, Lock
a = 0
def add(lock):
global a
for i in range(500000):
with lock:
a += 1
def sub(lock):
global a
for i in range(500000):
with lock:
a -= 1
lock = Lock()
t1 = Thread(target=add, args=(lock,))
t1.start()
t2 = Thread(target=sub, args=(lock,))
t2.start()
t1.join()
t2.join()
print(a)
互斥锁
# 在同一个线程中不能acquire多次,否则会死锁。
'房间相互独立,退出一个房间才能进另一个房间。'
from threading import Lock
lock = Lock()
lock.acquire()
print(666)
lock.acquire()
print(888)
# 分割线
lock = Lock()
lock.acquire()
print(666)
lock.release()
lock.acquire()
print(888)
lock.release()
单例模式
import time
from threading import Thread, Lock
class A:
__instance = None
def __new__(cls, *args, **kwargs):
if not cls.__instance:
time.sleep(0.1) # 强制移交GIL锁
cls.__instance = super().__new__(cls)
return cls.__instance
def __init__(self, name, age):
self.name = name
self.age = age
def func():
a = A('alex', 18)
print(a)
for i in range(10):
t = Thread(target=func)
t.start()
# 加锁才安全
import time
from threading import Thread, Lock
class A:
# from threading import Lock
__instance = None
lock = Lock()
def __new__(cls, *args, **kwargs):
with cls.lock:
if not cls.__instance:
time.sleep(0.1)
cls.__instance = super().__new__(cls)
return cls.__instance
def __init__(self, name, age):
self.name = name
self.age = age
def func():
a = A('alex', 18)
print(a)
for i in range(10):
t = Thread(target=func)
t.start()
死锁现象
import time
from threading import Thread, Lock
noodle_lock = Lock()
fork_lock = Lock()
def eat1(name, noodle_lock, fork_lock):
noodle_lock.acquire()
print('%s抢到面了' % name)
fork_lock.acquire()
print('%s抢到叉子了' % name)
print('%s吃了一口面' % name)
time.sleep(0.1)
fork_lock.release()
print('%s放下叉子了' % name)
noodle_lock.release()
print('%s放下面了' % name)
def eat2(name, noodle_lock, fork_lock):
fork_lock.acquire()
print('%s抢到叉子了' % name)
noodle_lock.acquire()
print('%s抢到面了' % name)
print('%s吃了一口面' % name)
time.sleep(0.1)
noodle_lock.release()
print('%s放下面了' % name)
fork_lock.release()
print('%s放下叉子了' % name)
lst = ['alex', 'wusir', 'taibai', 'yuan']
Thread(target=eat1, args=(lst[0], noodle_lock, fork_lock)).start()
Thread(target=eat2, args=(lst[1], noodle_lock, fork_lock)).start()
Thread(target=eat1, args=(lst[2], noodle_lock, fork_lock)).start()
Thread(target=eat2, args=(lst[3], noodle_lock, fork_lock)).start()
死锁现象发生原因?
- 多把锁交替使用
解决方案
- 递归锁
- 优点:解决问题耗时短
- 缺点:效率差
- 优化代码逻辑
- 优点:效率高(将多把互斥锁变成一把互斥锁)
- 缺点:解决问题耗时长
- 总结
- 死锁现象
- 多把锁交替使用
- 无论互斥锁还是递归锁,当创建多把且交替使用都会发生死锁现象。
- 多把锁交替使用
- 解决方式
- 创建递归锁:本质就是快速将多把互斥锁变成了一把递归锁(可连续acquire)。
- 优化代码逻辑:本质就是将多把交替使用的互斥锁变成一把互斥锁。
- 两个方式究其根源是一样的。
- 死锁现象
递归锁
# 在同一个线程中可以acquire多次,不会死锁。
'房间中套着房间,进了多少层房间就要退出多少层房间。'
from threading import RLock
rlock = RLock()
rlock.acquire()
print(1)
rlock.acquire()
print(2)
rlock.acquire()
print(3)
# 通过递归锁解决死锁现象
import time
from threading import Thread, RLock
# noodle_lock = fork_lock = RLock()
'如果创造两个递归锁对象,还是会发生死锁'
lock = RLock()
# def eat1(name, noodle_lock, fork_lock):
def eat1(name, lock)
# noodle_lock.acquire()
lock.acquire()
print('%s抢到面了' % name)
# fork_lock.acquire()
lock.acquire()
print('%s抢到叉子了' % name)
print('%s吃了一口面' % name)
time.sleep(0.1)
# fork_lock.release()
lock.release()
print('%s放下叉子了' % name)
# noodle_lock.release()
lock.release()
print('%s放下面了' % name)
def eat2(name, noodle_lock, fork_lock):
fork_lock.acquire()
print('%s抢到叉子了' % name)
noodle_lock.acquire()
print('%s抢到面了' % name)
print('%s吃了一口面' % name)
time.sleep(0.1)
noodle_lock.release()
print('%s放下面了' % name)
fork_lock.release()
print('%s放下叉子了' % name)
lst = ['alex', 'wusir', 'taibai', 'yuan']
Thread(target=eat1, args=(lst[0], noodle_lock, fork_lock)).start()
Thread(target=eat2, args=(lst[1], noodle_lock, fork_lock)).start()
Thread(target=eat1, args=(lst[2], noodle_lock, fork_lock)).start()
Thread(target=eat2, args=(lst[3], noodle_lock, fork_lock)).start()
# 互斥锁解决死锁现象
import time
from threading import Thread, Lock
lock = Lock()
def eat1(name, lock):
lock.acquire()
print('%s抢到面了' % name)
print('%s抢到叉子了' % name)
print('%s吃了一口面' % name)
time.sleep(0.1)
print('%s放下叉子了' % name)
print('%s放下面了' % name)
lock.release()
def eat2(name, lock):
lock.acquire()
print('%s抢到叉子了' % name)
print('%s抢到面了' % name)
print('%s吃了一口面' % name)
time.sleep(0.1)
print('%s放下面了' % name)
print('%s放下叉子了' % name)
lock.release()
lst = ['alex', 'wusir', 'taibai', 'yuan']
Thread(target=eat1, args=(lst[0], lock)).start()
Thread(target=eat2, args=(lst[1], lock)).start()
Thread(target=eat1, args=(lst[2], lock)).start()
Thread(target=eat2, args=(lst[3], lock)).start()
2.线程队列
区别
queue模块
- 实现线程间安全通信
multiprocessing模块中的Queue类
- 实现进程间安全通信
先进先出队列
from queue import Queue
q = Queue(5) # 先进先出队列
q.put(1)
q.put(2)
q.put(3)
q.put(4)
q.put(5)
print('444')
q.put(6) # 队列满了,发生阻塞
print('555') # 不会打印
from queue import Queue
q = Queue(5)
q.put(1)
q.put(2)
q.put(3)
q.put(4)
q.put(5)
print(q.get())
print(q.get())
print(q.get())
print(q.get())
print(q.get())
print(q.get()) # 队列为空,等待取值会阻塞。
后进先出队列
# 栈(last in first out)
from queue import LifoQueue
lfq = LifoQueue(3)
lfq.put(1)
lfq.put(2)
lfq.put(3)
print(lfq.get())
print(lfq.get())
print(lfq.get())
优先级队列
from queue import PriorityQueue
pq = PriorityQueue()
pq.put((3,'yuan'))
pq.put((2, 'wusir'))
pq.put((1,'alex'))
print(pq.get())
print(pq.get())
print(pq.get())
结果:
(1, 'alex')
(2, 'wusir')
(3, 'yuan')
# 优先级序列实现自动排序
应用场景
- 先进先出
- 服务相关
- Server端将所有用户请求放在队列中,先来先服务。
- 服务相关
- 后进先出
- 算发相关
- 三级菜单
- 算发相关
- 优先级
- VIP服务
- 抢票优先
- 运维
- 告警级别
- VIP服务
3.池
进程池
- 预先开启固定个数的进程组成一个进程池,当任务来临的时候直接提交给已经开好的进程池中的进程。
- 节省进程的开启、关闭、切换时间。
- 减轻操作系统调度负担。
# submit and shutdown
import os
import time
import random
from concurrent.futures import ProcessPoolExecutor
def func():
print('start', os.getpid())
time.sleep(random.randint(1, 3))
print('end', os.getpid())
if __name__ == '__main__':
p = ProcessPoolExecutor(5) # cpu的核心数+1
for i in range(10):
p.submit(func) # 提交任务给进程池
p.shutdown() # 关闭池之后,就不能继续提交任务,并且会阻塞直到已经提交的任务完成。
print('main', os.getpid())
# 参数 + 返回值
import os
import time
import random
from concurrent.futures import ProcessPoolExecutor
def func(i):
print('start', os.getpid())
time.sleep(random.randint(1, 3))
print('end', os.getpid())
return '%s * %s' %(i, os.getpid())
if __name__ == '__main__':
p = ProcessPoolExecutor(5)
ret_l = []
for i in range(10):
ret = p.submit(func, i) # 提交任务时可以传入参数
print(ret)
ret_l.append(ret)
for ret in ret_l:
print('>>>', ret.result()) # result 同步阻塞
print('main', os.getpid())
线程池
- 传统多进程和多线程方案会使用“即时创建, 即时销毁”的策略。尽管与创建进程相比,创建线程的时间已经大大的缩短,但是如果提交给线程的任务是执行时间较短,而且执行次数极其频繁,那么服务器将处于不停的创建线程,销毁线程的状态。
- 一个线程的运行时间可以分为3部分:线程的启动时间、线程体的运行时间和线程的销毁时间。在多线程处理的情景中,如果线程不能被重用,就意味着每次创建都需要经过启动、销毁和运行3个过程。这必然会增加系统相应的时间,降低了效率。
- 使用线程池:
由于线程预先被创建并放入线程池中,同时处理完当前任务之后并不销毁而是被安排处理下一个任务,因此能够避免多次创建线程,从而节省线程创建和销毁的开销,能带来更好的性能和系统稳定性。
import os
import time
import random
from concurrent.futures import ThreadPoolExecutor
def func(i):
print('start', os.getpid())
time.sleep(random.randint(1, 3))
print('end', os.getpid())
return '%s * %s' % (i, os.getpid())
tp = ThreadPoolExecutor(20) # cpu的核心数*5
lst = []
for i in range(10):i
obj = tp.submit(func, i) # ret是对象
lst.append(obj) # 对象加入列表
tp.shutdown() # 阻塞
for ret in lst:
print('>>>', obj.result()) # 对象.result() 获取返回值
import os
import time
import random
from concurrent.futures import ThreadPoolExecutor
def func(i):
print('start', os.getpid())
time.sleep(random.randint(1, 3))
print('end', os.getpid())
return '%s * %s' % (i, os.getpid())
tp = ThreadPoolExecutor(20) # cpu的核心数*5
ret = tp.map(func, range(10)) # 提交任务时迭代传入参数
for i in ret: # ret是生成器,每个元素是返回值。
print(i)
回调函数
import requests
from concurrent.futures import ThreadPoolExecutor
def get_page(url):
res = requests.get(url)
return {'url':url, 'content':res.text}
def parserpage(ret):
dic = ret.result()
print(dic['url'])
tp = ThreadPoolExecutor(5)
url_lst = [
'http://www.baidu.com',
'http://www.cnblogs.com',
'http://www.douban.com',
'http://www.tencent.com',
'https://www.cnblogs.com/elliottwave/'
]
for url in url_lst:
ret = tp.submit(get_page, url)
ret.add_done_callback(parserpage) # 哪个线程先结束就先调用parserpage函数
import requests
from concurrent.futures import ThreadPoolExecutor
def get_page(url):
res = requests.get(url)
return {'url':url, 'content':res.text}
def parserpage(dic):
print(dic['url'])
tp = ThreadPoolExecutor(5)
url_lst = [
'http://www.baidu.com',
'http://www.cnblogs.com',
'http://www.douban.com',
'http://www.tencent.com',
'https://www.cnblogs.com/elliottwave/'
]
ret_lst = []
for url in url_lst:
ret = tp.submit(get_page, url)
ret_lst.append(ret) # 顺序添加对象到列表
for ret in ret_lst:
dic = ret.result() # 如果先添加的对象在线程池中对应的线程未结束,这里就会阻塞等待返回结果。
parserpage(dic) # 效率低于使用回调函数的方法
4.总结
- 创建进程池、线程池
- 导入
concurrent.futures模块中的ProcessPoolExcutor类、ThreadPoolExcutor类 - p = ProcessPoolExcutor(池中进程个数)
- tp = ThreadPoolExcutor(池中线程个数)
- 导入
- 异步提交任务
- ret = p.submit(函数名, 参数1, 参数2, ...)
- ret = tp.submit(函数名, 参数1, 参数2, ...)
- 获取返回值
- ret.result()
- 阻塞直到所有任务执行完毕
- p.shutdown()
- tp.shutdown()
- map方法
- ret = tp.map(func, iterable)
- 迭代获取
iterable中的元素作为func的参数,让池中线程执行对应的任务。
- 迭代获取
- for i in ret:每一个
i都是func的返回值
- ret = tp.map(func, iterable)
- 回调函数
- ret.add_done_callback(函数名)
- 在ret对应的任务执行结束后直接继续执行
add_done_callback绑定的函数,且ret作为参数传入绑定函数。

浙公网安备 33010602011771号