GIL与普通互斥锁区别 验证多线程 死锁现象 信号量和event事件 进程池与线程池 协程

day37

 

GIL与普通互斥锁区别

GIL:

  在单核CPU上运行多个线程时,使用GIL可以保证系统资源只分配给一个线程,其它线程在GIL释放之前保持等待状态。

两者区别:

  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无用武之地   多进程 利用多核  速度更快
多线程 节约资源 切换+保存状态   多线程 速度较慢

结论:多进程和多线程都有具体的应用场景  尤其是多线程并不是没有用

 

代码验证:

from threading import Thread
from multiprocessing import Process
import os
import time


def work():
    res = 1
    for i in range(1, 10000):
        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()
    t_list = []
    for i in range(100):
        t = Thread(target=work)
        t.start()
    for t in t_list:
        t.join()
    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
    两者差了两个数量级
结论
    多线程更好
"""

 

死锁现象

  在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。

死锁一旦产生就会造成应用程序的停止响应,应用程序无法再继续往下执行了。

from threading import Thread, Lock
import time

# 产生两把锁  20个线程来抢锁
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()  # 起了20个线程
    t.start()
"""锁不能轻易使用并且以后我们也不会在自己去处理锁都是用别人封装的工具"""

 

semaphore信号量(了解)

多线程同时运行,能提高程序的运行效率,但是并非线程越多越好,而semaphore信号量可以通过内置计数器来控制同时运行线程的数量,启动线程(消耗信号量)内置计数器会自动减一,线程结束(释放信号量)内置计数器会自动加一;内置计数器为零,启动线程会阻塞,直到有本线程结束或者其他线程结束为止;

semaphore信号量相关函数介绍

acquire() — 消耗信号量,内置计数器减一;

release() — 释放信号量,内置计数器加一;

在semaphore信号量有一个内置计数器,控制线程的数量,acquire()会消耗信号量,计数器会自动减一;release()会释放信号量,计数器会自动加一;当计数器为零时,acquire()调用被阻塞,直到release()释放信号量为止。

信号量在不同的知识体系中 展示出来的功能是不一样的
	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. 支持高并发

线程池

from concurrent.futures import ThreadPoolExecutor

pool = ThreadPoolExecutor(5)

括号内不传参数的话,会默认开设当前计算机cpu核数的5倍的线程(看源码即懂),假设参数写5,代表池子里面固定有5个线程,不存在不断创建和销毁的过程。
也就是说你是老板,你请的是5个固定的员工,而不是来来去去的临时工。避免了重复创建线程的开销。
进程池

from concurrent.futures import ProcessPoolExecutor
pool = ProcessPoolExecutor(3)

进程池大体上跟线程池类似,ProcessPoolExecutor(3),括号内不填的话,会默认创建与“cpu核数”相同数量的进程(看源码即懂),同样的,进程池中的进程是固定工,不会重复创建和销毁。

进程池和线程池在使用形式上是一样的,唯一不同的是:在Windows环境下,进程池要放在main方法里面,否则会报错
回顾:
	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)

 

协程

通常我们认为线程是轻量级的进程,因此我们也把协程理解为轻量级的线程即微线程。

通常在Python中我们进行并发编程一般都是使用多线程或者多进程来实现的,对于计算型任务由于GIL的存在我们通常使用多进程来实现,而对于IO型任务我们可以通过线程调度来让线程在执行IO任务时让出GIL,从而实现表面上的并发。其实对于IO型任务我们还有一种选择就是协程,协程是运行在单线程当中的"并发",协程相比多线程一大优势就是省去了多线程之间的切换开销,获得了更大的运行效率。

协程,又称微线程,纤程,英文名Coroutine。协程的作用是在执行函数A时可以随时中断去执行函数B,然后中断函数B继续执行函数A(可以自由切换)。但这一过程并不是函数调用,这一整个过程看似像多线程,然而协程只有一个线程执行。

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

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

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

 

基于协程实现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 @ 2022-04-21 23:06  ji哩咕噜  阅读(46)  评论(0编辑  收藏  举报