网络编程(六)
网络编程(六)
昨日内容回顾
消息队列
from multiprocessing import Queue
队列模块可以为IPC机制和生产者消费模型提供数据交互的容器
IPC机制
利用队列对进程间的数据进行交互
生产者消费者模型
不同进程对队列中的数据进行平衡的生产和消费
线程理论
进程:资源单位,产生空间、代码
线程:执行单位,运转代码
- 进程中必须拥有一到多条线程
- 进程内的线程数据共享
- 开设线程消耗的资源远远小于开设进程
线程开展的方式
from threading import Thread # 调用模块
方式1
t = Thread()
t.start()
方式2
class MyThread(Thread):
def run(self):pass
t_obj = MyThread()
t_obj.start()
线程join方法和守护线程
join方法:主线程等待子线程代码运行完毕之后再往后运行
守护线程:主线程需要等待所有非守护线程结束才能结束
线程对象数据和方法
current_thread().name # 获取线程名
self.name # 获取线程名
active_count() # 获取当前存活的线程(主线程也算)
GIL全局解释器锁
1.GIL是Cpython解释器的特点,不是python语言的特点
Cpython解释器是最常用的,不刻意指定几乎默认都是它!!!
2.GIL的存在是为了防止CPython解释器垃圾回收机制造成数据不安全
3.GIL本质其实也是一把互斥锁 只不过它用于维护解释器级别的数据安全
针对不同的数据应该加不同的锁
GIL保护的是代码数据的正常执行
用户自定义的互斥锁保护的是业务数据的安全 抢票
4.python同一个进程下的多个线程无法利用多核优势
![]()
走入今天的学习隧道
今日学习内容
GIL与普通互斥锁的区别
# 1.先验证GIL的存在
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作用面很窄 仅限于解释器级别
后期我们要想保证数据的安全应该自定义互斥锁(使用别人封装好的工具)
"""
验证多线程作用
多进程
| 单个CPU | 多个CPU | |
|---|---|---|
| IO密集型 | 浪费资源 无法利用多个CPU | 浪费资源 多个CPU无用武之地 |
| 计算密集型 | 耗时更长 创建进程的消耗+切换消耗 | 利用多核 速度更快 |
多线程
| 单个CPU | 多个CPU | |
|---|---|---|
| IO密集型 | 节省资源 切换+保存状态 | 节省资源 切换+保存状态 |
| 计算密集型 | 耗时较短 切换消耗 | 速度较慢 |
多线程和多进程各有其应用场景。
死锁现象
我们举个例子来表示死锁现象:
现在有两把锁两把钥匙和两个人,我们分别取名为A锁 A钥匙 和小A、B锁 B钥匙和小B。现在小A需要打开A锁,但是手上只有B钥匙,没办法打开A锁,现在这个A钥匙在小B手上,就需要等待小B释放这把A钥匙;而小B现在的情况和小A如出一辙,他需要打开B锁才能将A钥匙释放掉,而B钥匙现在在小A手上。这就产生了一个无限等待对方释放钥匙,而又因为释放钥匙的前提无法达成,从而产生了一个死锁现象。
小A需要A钥匙🔑 来打开 🔒 A锁,而他只有B钥匙🔑
小B需要B钥匙🔑 来打开 🔒 B锁,而他只有A钥匙🔑
信号量
信号量有的时候也被称为信号灯,在多线程环境下类比锁;
可以用来保证两个或多个关键数据不被并发调用。在进入一个关键数据之前,线程必须获取一个信号量(锁);一旦该关键数据操作完成了,那么该线程必须释放信号量(锁)。其它想进入该关键代码段的线程必须等待直到 第一个线程释放信号量(锁)。
代码中体现
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事件
event就相当于创建一个标识,标识会提供布尔值。我们在运行其他子进程的时候,可以设置条件走过经过这个标识,根据不同的布尔值执行不同的结果。
"""
子线程的运行可以由其他子线程决定!!!
"""
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简便)
进程池与线程池
服务端三要素:
- 24小时不停服
- 固定的地址(ip)和port(端口号)
- 高并发
只实用多进程和多线程去完成服务端的需求,都不太符合现实逻辑
这时候我们就需要进程池与线程池
进程池:
进程池的概念,定义一个池子,在里面放上固定数量的进程,有需求来了,就拿一个池中的进程来处理任务,等到处理完毕,进程并不关闭,而是将进程再放回进程池中继续等待任务。如果有很多任务需要执行,池中的进程数量不够,任务就要等待之前的进程执行任务完毕归来,拿到空闲进程才能继续执行。也就是说,池中进程的数量是固定的,那么同一时间最多有固定数量的进程在运行。这样不会增加操作系统的调度难度,还节省了开闭进程的时间,也一定程度上能够实现并发效果。
线程池:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
# 代码
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)
协程
进程:资源单位
线程:执行单位
并发:切换+保存状态
协程:单线程下实现并发
协程实际上是程序员根据产生的功能拟定出的名词,对于操作系统来说只存在进程和线程
协程就是自己通过代码检测程序的IO操作并处理,让CPU感觉不到IO操作从而最大程度占用CPU
就是让CPU不因为IO操作的而降低CPU的工作效率,简单说就是继续压榨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+
# 5s+ 代码控制切换
基于协程实现TCP服务端并发
from gevent import monkey;monkey.patch_all()
from gevent import spawn
import socket
def communication(sock):
while True:
data = sock.recv(1024) # IO操作
print(data.decode('utf8'))
sock.send(data.upper())
def get_server():
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
sock, addr = server.accept() # IO操作
spawn(communication, sock)
g1 = spawn(get_server)
g1.join()
"""
终极结论
python可以通过开设多进程 在多进程下开设多线程 在多线程使用协程
从而让程序执行的效率达到极致!!!
但是实际业务中很少需要如此之高的效率(一直占着CPU不放,烧CPU)
因为大部分程序都是IO密集型的
所以协程我们知道它的存在即可 几乎不会真正去自己编写
"""
今日小结
万众瞩目的前端学习,明天就来了!

浙公网安备 33010602011771号