day(线程池)
今日内容详细
昨日内容回顾:
消息队列可以实现进程间通信(本地、网络),并且消息队列还起到了保存数据的功能(队列中的数据如果不被取走,会一直在队列中)
"""ps:课下可以百度搜搜消息队列"""
消息队列将生产者与消费者解耦合!!!
生产者只需要将数据放入队列中即可 无需考虑是否有人消费
消费者之需要将数据从队列中取出 无需考虑是否有生产者生产
#系统解耦
系统之间不是 强耦合的,消息接受者 可以随意增加,而不需要修改 消息发送者的代码。消息发送者 的成功不依赖 消息接受者(比如:有些银行接口不稳定,但调用方并不需要依赖这些接口)。
不强依赖 于非本系统的核心流程,对于 非核心流程,可以放到消息队列中让 消息消费者 去按需消费,而 不影响核心主流程。
#IPC机制
就是利用队列进行进程间的数据交互
#线程理论

进程其实是资源单位
负责提供线程执行过程中所需的各项资源
线程才是执行单位
真正在进程中干活的人(借助于解释器与CPU交互的)
"""
1.进程中肯定都自带至少一个线程
2.进程内可以创建多个线程
3.进程内多个线程数据共享
4.开设线程的资源消耗远远小于开设进程
"""
线程的join方法与进程join方法一致
主线程等待子线程代码运行完毕之后再往后运行
""""主线程需要等待所有非守护线程结束才能结束"""
1.GIL是Cpython解释器的特点,不是python语言的特点
Cpython解释器是最常用的,不刻意指定几乎默认都是它!!!
2.GIL的存在是为了防止CPython解释器垃圾回收机制造成数据不安全
3.GIL本质其实也是一把互斥锁 只不过它用于维护解释器级别的数据安全
针对不同的数据应该加不同的锁
GIL保护的是代码数据的正常执行 name = 'jason'
用户自定义的互斥锁保护的是业务数据的安全 抢票
4.python同一个进程下的多个线程无法利用多核优势
今日内容的详细、
GIL(Global Interpret Lock):全局解释器锁,是锁在 CPython 解释器上,导致同一时刻,同一进程只能有一个线程被执行。互斥锁是在多线程的情况下,确保当前线程执行完成后,再执行下一个任务,当前任务没有结束,下个任务会阻塞。
GIL是保证同一时间只有1个线程在执行,但是该线程让出GIL的时,有可能并没完成该线程的任务,该线程的任务分多少次执行完成这个会安装GIL的默认策略。、
GIL与普通互斥锁的区别
1 GIL锁是不能保证数据的安全,普通互斥锁来保证数据安全
二个需要注意的点:
#1.线程抢的是GIL锁,GIL锁相当于执行权限,拿到执行权限后才能拿到互斥锁Lock,其他线程也可以抢到GIL,但如果发现Lock仍然没有被释放则阻塞,即便是拿到执行权限GIL也要立刻交出来
#2.join是等待所有,即整体串行,而锁只是锁住修改共享数据的部分,即部分串行,要想保证数据安全的根本原理在于让并发变成串行,join与互斥锁都可以实现,毫无疑问,互斥锁的部分串行效率要更高
# 1.先验证GIL的存在
from threading import Thread, Lock
import time
money = 100
def task():
global money
money -= 1
for i in range(100): # 创建一百个线程
t = Thread(target=task)
t.start()
print(money)
# 2.再验证不同数据加不同锁
from threading import Thread, Lock
import time
money = 100
mutex = Lock()
def task():
global money
mutex.acquire()
tmp = money
time.sleep(0.1)
money = tmp - 1
mutex.release()
"""
抢锁放锁也有简便写法(with上下文管理)
with mutex:
pass
"""
t_list = []
for i in range(100): # 创建一百个线程
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
# 为了确保结构正确 应该等待所有的线程运行完毕再打印money
print(money)
"""
GIL是一个纯理论知识 在实际工作中根本无需考虑它的存在
GIL作用面很窄 仅限于解释器级别
后期我们要想保证数据的安全应该自定义互斥锁(使用别人封装好的工具)
"""
验证多线程作用
.开多个进程与线程时间上的对比
多进程
from multiprocessing import Process
import os, time
def task():
print("多进程")
if __name__ == '__main__':
pc_li = []
start_time = time.time()
for i in range(1000):
p = Process(target=task)
p.start()
pc_li.append(p)
for i in pc_li:
i.join()
print("用时:%s" % (time.time() - start_time))
print(f"{os.getpid()}end")
用时: 14.129……多
# 多线程
from threading import Thread
import os, time
def task():
print("多线程")
if __name__ == '__main__':
pc_li = []
start_time = time.time()
for i in range(1000):
p = Thread(target=task)
p.start()
pc_li.append(p)
for i in pc_li:
i.join()
print("用时:%s" % (time.time() - start_time)) # 用时:0.0019948482513427734
print(f"{os.getpid()}end")
# 1000
# 个进程用时 14.129……多
# 1000
# 个线程用时
# 0.1秒多
- 运算密集型对比
from multiprocessing import Process
from threading import Thread
import time,os
多进程测试
def calculate():
count = 0
while count < 30000000:
count +=1
if __name__ == '__main__':
li = []
print(os.cpu_count()) # 查看CPU个数--->4
start_time = time.time()
for i in range(4):
p = Process(target=calculate)
p.start()
li.append(p)
for i in li:
i.join()
print(f"多进程用时:{time.time()-start_time}")
#多进程用时:5.714418649673462
多线程测试
def calculate():
count = 0
while count < 30000000:
count +=1
if __name__ == '__main__':
li = []
print(os.cpu_count()) # 查看CPU个数--->4
start_time = time.time()
for i in range(4):
p = Thread(target=calculate)
p.start()
li.append(p)
for i in li:
i.join()
print(f"多线程用时:{time.time()-start_time}")
#多线程用时:9.828791618347168
- I/O密集型对比
from multiprocessing import Process
from threading import Thread
import time,os
多进程测试
def calculate():
time.sleep(2)
if __name__ == '__main__':
li = []
print(os.cpu_count()) # 查看CPU个数--->4
start_time = time.time()
for i in range(500):
p = Process(target=calculate)
p.start()
li.append(p)
for i in li:
i.join()
print(f"多进程用时:{time.time()-start_time}")
#多进程用时:40.83470439910889
多线程测试
def calculate():
time.sleep(2)
print("完毕--->")
if __name__ == '__main__':
li = []
print(os.cpu_count()) # 查看CPU个数--->4
start_time = time.time()
for i in range(500):
p = Thread(target=calculate)
p.start()
li.append(p)
for i in li:
i.join()
print(f"多线程用时:{time.time()-start_time}")
#多线程用时:2.056976556777954
: Cpython 对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于 I/O 密集型的任务效率还是有显著提升的
应用场景
多线程主要运用于I/O密集型 : socket, 爬虫, web
多进程主要运用于计算密集型 : 金融分析, 比特币挖矿
"""
两个大前提
CPU的个数
单个
多个
任务的类型
IO密集型
计算密集型
"""
# 单个CPU
多个IO密集型任务
多进程:浪费资源 无法利用多个CPU
多线程:节省资源 切换+保存状态
多个计算密集型任务
多进程:耗时更长 创建进程的消耗+切换消耗
多线程:耗时较短 切换消耗
# 多个CPU
多个IO密集型任务
多进程:浪费资源 多个CPU无用武之地
多线程:节省资源 切换+保存状态
多个计算密集型任务
多进程:利用多核 速度更快
多线程:速度较慢
结论:多进程和多线程都有具体的应用场景 尤其是多线程并不是没有用!!!
'''代码验证'''
from threading import Thread
from multiprocessing import Process
import os
import time
from threading import Thread
from multiprocessing import Process
import os
import time
#
def work():
res = 1
for i in range(1, 10000000):
res *= i
if __name__ == '__main__':
print(os.cpu_count()) # 12 查看当前计算机CPU个数
start_time = time.time()
p_list = []
for i in range(12):
p = Process(target=work)
p.start()
p_list.append(p)
for p in p_list:
p.join()
t_list = []
for i in range(12):
t = Thread(target=work)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print('总耗时:%s' % (time.time() - start_time))
"""
计算密集型
多进程
0.08273792266845703
多线程
0.28725099563598633
两者差了一个数量级(越多差距越大)
结论
多进程更好
"""
def work():
time.sleep(2) # 模拟纯IO操作
if __name__ == '__main__':
start_time = time.time()
p_list = []
for i in range(100):
p = Process(target=work)
p.start()
for p in p_list:
p.join()
print('总耗时:%s' % (time.time() - start_time))
"""
IO密集型
多线程
总耗时:0.007348060607910156
多进程
总耗时:0.1564030647277832
两者差了两个数量级
结论
多线程更好
"""
死锁现象
我们先来了解一下什么是死锁?
所谓死锁 : 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
#🔹老板向员工发工资, 员工向老板还钱, 财务负责处理金额问题, 处理财务必须获得个人令牌,
#🔹老板--->员工 : 财务1先获得了老板令牌, 再获得员工两人的令牌, 进行第一次金额交易后归还了各自令牌
#🔹员工--->老板 : 紧接着财务1看看账目, 发现员工要向老板还钱, 于是先获取员工的令牌, 于此同时财务2在另外一本账目上处理一笔老板向员工的转账
#🔹于是乎财务2将老板的令牌拿走了, 财务1没拿到,而财务2想拿员工的令牌, 发现令牌在财务1手上,于是两人争执不过就大打出手了
from threading import Thread,Lock
import time
boss = Lock()
staff = Lock()
class Mythread(Thread):
def __init__(self,name):
super().__init__()
self.name = name
def deal1(self):
boss.acquire() # 加老板锁
print(f"{self.name}获取了老板令牌")
staff.acquire() # 加员工锁
print(f"{self.name}获取了员工令牌")
print(f"{self.name}操控了交易")
staff.release() # 释放老板锁
boss.release() # 释放员工锁
def deal2(self):
staff.acquire() # 加员工锁
print(f"{self.name}获得了员工令牌")
print("正在去老板办公室...")
time.sleep(1)
boss.acquire() # 加老板锁
print(f"{self.name}获得了老板令牌")
print(f"没内鬼,{self.name}继续交易")
boss.release() # 释放老板锁
staff.release() # 释放员工锁
def run(self):
self.deal1()
self.deal2()
if __name__ == '__main__':
p1 = Mythread("财务1")
p2 = Mythread("财务2")
p1.start()
p2.start()
'''输出
财务1获取了老板令牌
财务1获取了员工令牌
财务1操控了交易
财务1获得了员工令牌
正在去老板办公室...
财务2获取了老板令牌
'''
出现死锁, 程序就此停在这无法再运行下去
信号量
信号量在不同的知识体系中 展示出来的功能是不一样的
线程信号量与进程信号量一模一样, 概念不在赘述, 下面举个例子 :
一个小网吧, 最多容纳3个网瘾少年, 可以同时有十个少年上机玩游戏, 其他人只能等着, 有些人完的时间短下机了, 那么就空出了一个位置(锁), 外边等的人就可以抢这个位置了
from threading import Thread,Semaphore,current_thread
import time,random
sm = Semaphore(3)
def young():
with sm:
print(f"少年{current_thread().name}正在打电脑")
time.sleep(random.randint(1,3))
print(f"少年{current_thread().name}下机了")
if __name__ == '__main__':
li = []
for i in range(7):
p = Thread(target=young)
p.start()
li.append(p)
for i in li:
i.join()
print("网吧老板被抓")
'''输出
少年Thread-1正在打电脑
少年Thread-2正在打电脑
少年Thread-3正在打电脑
少年Thread-2下机了
少年Thread-4正在打电脑
少年Thread-4下机了
少年Thread-5正在打电脑
少年Thread-3下机了
少年Thread-1下机了
少年Thread-6正在打电脑
少年Thread-7正在打电脑
少年Thread-5下机了
少年Thread-6下机了
少年Thread-7下机了
网吧老板被抓
Process finished with exit code 0
'''
信号量在不同的知识体系中 展示出来的功能是不一样的
eg:
在并发编程中信号量意思是多把互斥锁
在django框架中信号量意思是达到某个条件自动触发特定功能
"""
如果将自定义互斥锁比喻成是单个厕所(一个坑位)
那么信号量相当于是公共厕所(多个坑位)
"""
from threading import Thread, Semaphore
import time
import random
sp = Semaphore(5) # 创建一个有五个坑位(带门的)的公共厕所
def task(name):
sp.acquire() # 抢锁
print('%s正在蹲坑' % name)
time.sleep(random.randint(1, 5))
sp.release() # 放锁
for i in range(1, 31):
t = Thread(target=task, args=('伞兵%s号' % i, ))
t.start()
# 只要是跟锁相关的几乎都不会让我们自己去写 后期还是用模块
event事件
线程之间状态同步, 两个不同的任务执行, 一个任务如果需要另一个任务执行之后才能开始执行, 那么这个待执行的任务是如何获取到上一个任务执行状态的呢? 如果是进程, 那就需要借助共享内存传递一个标志信号, 而线程本身就共享一个线程的内存空间, 所以, 为了解决以上问题, threading 模块为我们提供了一个 Event 对象
1.Event 对象的方法
Event 本质就是一个标志, True或者False, 而它提供的wait函数可以阻塞当前线程, 直到状态从False变为True
"""
子线程的运行可以由其他子线程决定!!!
"""
from threading import Thread, Event
import time
event = Event() # 类似于造了一个红绿灯
def light():
print('红灯亮着的 所有人都不能动')
time.sleep(3)
print('绿灯亮了 油门踩到底 给我冲!!!')
event.set()
def car(name):
print('%s正在等红灯' % name)
event.wait()
print('%s加油门 飙车了' % name)
t = Thread(target=light)
t.start()
for i in range(20):
t = Thread(target=car, args=('熊猫PRO%s' % i,))
t.start()
# 这种效果其实也可以通过其他手段实现 比如队列(只不过没有event简便)
导入方法 : from threading import Event
方法 | 作用 |
---|---|
event.isSet( ) | 返回event的状态 |
event.wait( ) | 状态值为False为阻塞, 默认False |
event.set( ) | 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度 |
event.clear( ) | 恢复event的状态值为False |
进程池与线程池(重点)
每学到一个新的知识我们都会问一下这个东西是什么,它有什么作用,学习他的目的是什么,为什么使用它。。
这两个池子也是不例外的
1.什么是线程池
与进程池类似, 线程池是在系统启动时就先创建大量空闲的线程, 程序提交一个任务给线程池, 线程池便会调用一个线程来执行该任务, 当任务运行完毕后, 该线程并不会关闭, 而是返回到线程池中再次变为空闲状态等待下一个提交的任务,
2.为什么使用线程池
虽说线程的启动相比较于进程开销非常小, 但毕竟也是需要向操作系统发起调用, 我们使用线程在一些情况下能更好的提升性能, 尤其是程序中有大量生命期短暂的线程时, 使用线程池最为合适了
3.线程池的作用
使用线程池可以精确控制操作系统中并发线程的数量, 如果操作系统中有大量的并发线程, 并且没有限制数量, 那么就会导致操作系统的性能急剧下降, 甚至导致程序的崩溃, 而线程池可以通过控制最大线程数来解决该问题
"""
补充:
服务端必备的三要素
1.24小时不间断提供服务
2.固定的ip和port
3.支持高并发
回顾:
TCP服务端实现并发
多进程:来一个客户端就开一个进程(临时工)
多线程:来一个客户端就开一个线程(临时工)
问题:
计算机硬件是有物理极限的 我们不可能无限制的创建进程和线程
措施:
池:
保证计算机硬件安全的情况下提升程序的运行效率
进程池:
提前创建好固定数量的进程 后续反复使用这些进程(合同工)
线程池:
提前创建好固定数量的线程 后续反复使用这些线程(合同工)
如果任务超出了池子里面的最大进程或线程数 则原地等待
强调:
进程池和线程池其实降低了程序的运行效率 但是保证了硬件的安全!!!"""
代码演示
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
# 线程池
pool = ThreadPoolExecutor(5) # 线程池线程数默认是CPU个数的五倍 也可以自定义
'''上面的代码执行之后就会立刻创建五个等待工作的线程'''
'''不应该自己主动等待结果 应该让异步提交自动提醒>>>:异步回调机制'''
pool.submit(task, i).add_done_callback(func)
"""add_done_callback只要任务有结果了 就会自动调用括号内的函数处理"""
# 进程池
pool = ProcessPoolExecutor(5) # 进程池进程数默认是CPU个数 也可以自定义
'''上面的代码执行之后就会立刻创建五个等待工作的进程'''
pool.submit(task, i).add_done_callback(func)
ThreadPoolExecutor使用示例
from concurrent.futures import ThreadPoolExecutor
import os,time,random
def task(n):
print(f"子线程:{os.getpid()}正在执行")
time.sleep(random.randint(1,3)) # 模拟任务执行时间
return n**2
if __name__ == '__main__':
thread_pool = ThreadPoolExecutor(max_workers=4) # 设置线程池大小
futures = []
for i in range(1,10):
future = thread_pool.submit(task,i) # 开启十个任务
futures.append(future)
thread_pool.shutdown(True) # 关闭线程池,并等待任务结束
for future in futures:
print(future.result()) # 循环取出任务运行的结果(等到左右的任务执行完后才拿到)
'''输出
子线程:1360正在执行
子线程:1360正在执行
子线程:1360正在执行
子线程:1360正在执行
子线程:1360正在执行
子线程:1360正在执行
子线程:1360正在执行
子线程:1360正在执行
子线程:1360正在执行
1
4
9
16
25
36
49
64
81
'''
使用回调函数爬取各网站大小示例
from concurrent.futures import ThreadPoolExecutor
import requests,os
def get_htm(url):
print(f"线程:{os.getpid()}正在获取网站:{url}源码")
response = requests.get(url)
if response.status_code == 200:
return {"url":url,"text":response.text}
else:
return {"url":url,"text":""}
def parse_htm(back):
res = back.result()
print(f"线程:{os.getpid()}正在解析网站:{url}源码")
with open("html_size.txt","a")as f:
f.write(f"url:{res['url']},size:{len(res['text'])}\n")
if __name__ == '__main__':
urls=[
'https://zhuanlan.zhihu.com',
'https://www.cnblogs.com',
'https://www.python.org',
'https://blog.csdn.net',
'http://www.china.com.cn',
]
li = []
thread_pool = ThreadPoolExecutor(3)
for url in urls:
future = thread_pool.submit(get_htm,url)
future.add_done_callback(parse_htm)
li.append(future)
thread_pool.shutdown(True)
协程、
协程 ,又称为微线程,它是实现多任务的另一种方式,只不过是比线程更小的执行单元。因为它自带CPU的上下文,这样只要在合适的时机,我们可以把一个协程切换到另一个协程。
通俗的理解: 在一个线程中的某个函数中,我们可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的 ,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定。
协程与线程的差异:
在实现多任务时, 线程切换__从系统层面__远不止保存和恢复CPU上下文这么简单。操作系统为了程序运行的高效性,每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作,所以线程的切换非常耗性能。但是__协程的切换只是单纯地操作CPU的上下文__,所以一秒钟切换个上百万次系统都抗的住。
"""
进程:资源单位
线程:执行单位
协程:单线程下实现并发
并发的概念:切换+保存状态
首先需要强调的是协程完全是程序员自己意淫出来的名词!!!
对于操作系统而言之认识进程和线程
协程就是自己通过代码来检测程序的IO操作并自己处理 让CPU感觉不到IO的存在从而最大幅度的占用CPU
类似于一个人同时干接待和服务客人的活 在接待与服务之间来回切换!!!
"""
# 基本使用
# 保存的功能 我们其实接触过 yield 但是无法做到检测IO切换
from gevent import monkey;monkey.patch_all() # 固定编写 用于检测所有的IO操作
from gevent import spawn
import time
def play(name):
print('%s play 1' % name)
time.sleep(5)
print('%s play 2' % name)
def eat(name):
print('%s eat 1' % name)
time.sleep(3)
print('%s eat 2' % name)
start_time = time.time()
g1 = spawn(play, 'jason')
g2 = spawn(eat, 'jason')
g1.join() # 等待检测任务执行完毕
g2.join() # 等待检测任务执行完毕
print('总耗时:', time.time() - start_time) # 正常串行肯定是8s+
# 5.00609827041626 代码控制切换
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下