• GIL与普通互斥锁区别

  • 验证多线程是否有用

  • 死锁现象(强调锁不能随便使用)

  • 信号量与event事件

  • 进程池与线程池

  • 协程

 

 

GIL与普通锁互斥锁区别

# 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作用面很窄 仅限于解释器级别
后期我们想要保证数据的安全应该自定义互斥锁(使用别人封装好的工具
)
"""

验证多线程作用

"""
两个大前提
CPU的个数
单个
多个
任务的类型
IO密集型
计算密集型
"""
# 单个CPU
多个IO密集型任务
  多进程:浪费资源 无法利用多个CPU
  多线程:节省资源 切换+保存状态
   多个计算密集型任务
  多进程:耗时更长 创建进程的消耗+切换消耗
       多线程:耗时较短 切换消耗
# 多个CPU
多个IO密集型任务
  多进程:浪费资源 多个CPU无用武之地
  多线程:节省资源 切换+保存状态
   多个计算密集型任务
  多进程:利用多核 速度更快
       多线程: 速度较慢
结论:多进程和多线程都有具体的应用场景 尤其是多线程并不是没有用

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()
   print('总耗时:%s' % (time.time() - start_time))
   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.4248642921447754
多线程
总耗时:0.6830117702484131

结论:多进程更好
"""

def work():
   time.sleep(2)   # 模拟纯IO操作


if __name__ == '__main__':
   start_time = time.time()
   t_list = []
   for i in range(100):
       t = Thread(target=work)
       t.start()
   for t in t_list:
       t.join()
   print('总耗时:%s' % (time.time() - start_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.014949560165405273
多进程
总耗时:0.4863429069519043
两者差了一个数量级
结论:多线程更好
"""

死锁现象

# 锁就算掌握了如何抢 如何放 也会产生死锁现象
from threading import Thread, Lock
import time

# 产生两把
mutexA = Lock()
mutexB = Lock()


class MyThread(Thread):
   def run(self):
       self.f1()
       self.f2()
   def f1(self):
       mutexA.acquire()
       print(f'{self.name}抢到了A锁')
       mutexB.acquire()
       print(f'{self.name}抢到了B锁')
       mutexB.release()
       mutexA.release()
   def f2(self):
       mutexB.acquire()
       print(f'{self.name}抢到了B锁')
       time.sleep(2)
       mutexA.acquire()
       print(f'{self.name}抢到了A锁')
       mutexA.release()
       mutexB.release()

for i in range(20):
   t = MyThread()
   t.start()
"""锁不能轻易使用并且以后我们也不会在自己去处理锁都是用别人封装的工具"""

信号量

信号量在不同的知识体系中 展示出来的功能是不一样的
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事件

"""
子线程的运行可以由其他子线程决定
"""
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简便)

进程池与线程池

"""
补充:
服务端必备的三要素
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)

协程

"""
进程:资源单位
线程:执行单位
协程:单线程下实现并发

并发的概念:切换+保存状态

首先需要强调的是协程完全是程序员自己意淫出来的名词
对于操作系统而言只认识进程和线程
协程就是自己通过代码来检测程序的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) # 5.01336121016525 代码控制切换 正常串行肯定是8s+

基于协程实现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不放)
因为大部分被程序都是IO密集型的

所有协程我们知道它的存在即可 几乎不会真正去自己编写
"""

 

 



 posted on 2022-04-21 21:57  鳗鱼的春天  阅读(129)  评论(0编辑  收藏  举报