python学习-Day37
阅读原文时间:2023年07月08日阅读:2

目录

今日内容详细

GIL与普通互斥锁区别

同:

都是为了解决解释器中多个线程资源竞争的问题

异:

1.互斥锁是Python代码层面的锁,解决Python程序中多线程共享资源的问题(线程数据共共享,当各个线程访问数据资源时会出现竞争状态,造成数据混乱);

2.GIL是Python解释层面的锁,解决解释器中多个线程的竞争资源问题(多个子线程在系统资源竞争是,都在等待对象某个部分资源解除占用状态,结果谁也不愿意先解锁,然后互相等着,程序无法执行下去)。

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

GIL对程序的影响

1.Python中同一时刻有且只有一个线程会执行;
2.Python中的多个线程由于GIL锁的存在无法利用多核CPU;
3.Python中的多线程不适合计算机密集型的程序;
4.如果程序需要大量的计算,利用多核CPU资源,可以使用多进程来解决。

验证多线程作用

两个大前提

# CPU的个数:
    单个
    多个

# 任务的类型:
    # IO密集型:系统运作,大部分的状况是CPU 在等I/O (硬盘/内存)的读/写。
    IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言
替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是
开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。

    # 计算密集型(CPU密集型):大部份时间用来做计算、逻辑判断等CPU 动作的程序称之CPU 密集型(计算密集型)。
    计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率
很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。

"""
多进程适合在CPU 密集型操作(cpu 操作指令比较多,如位数多的浮点运算)。
多线程适合在IO 密集型操作(读写数据操作较多的,比如爬虫)。
"""
关于CPU的个数
# 单个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

# 产生两把(复习 面向对象和单例模式):每天都可以写写单例啊 算法啊...
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()
"""锁不能轻易使用并且以后我们也不会在自己去处理锁都是用别人封装的工具"""

避免死锁的解决:

1.重构代码
2.添加超时释放锁

添加超时释放锁

    from threading import Thread,Lock
    import time

    lockA = Lock()
    lockB = Lock()

    #自定义线程
    class MyThread1(Thread):
        #不论进程还是线程重写的都是run方法
        def run(self):
            if lockA.acquire():#如果可以获取到锁则返回True
                print(self.name +'A锁')
                time.sleep(0.1)
                if lockB.acquire(timeout=3):#在acquire函数中阻塞,一直等待锁,不能往下运行了
                                            #如果加上超时则表示退出acquire,继续往下执行把a锁释放了
                    print(self.name +"A锁+B锁")
                    lockB.release()
                lockA.release()

    #自定义线程
    class MyThread2(Thread):
        #不论进程还是线程重写的都是run方法
        def run(self):
            if lockB.acquire():#如果可以获取到锁则返回True
                print(self.name +'B锁')
                time.sleep(0.1)
                if lockA.acquire(timeout=3):
                    print(self.name +"A锁+B锁")
                    lockA.release()
                lockB.release()

    def main():
        pass

    if __name__ == "__main__":

    #若不加上超时,则会一直不能进入A锁+B锁情况。
    #造成线程1,2一直死等
        MyThread1().start()
        MyThread2().start()

信号量

信号量:是最古老的同步原语之一,是一个计数器

当资源释放时计数器就会递增,当资源申请时计数器就会递减。可以认为信号量就代表着资源是否可用

信号量在不同的知识体系中 展示出来的功能是不一样的
    eg:
    在并发编程中信号量意思是多把互斥锁
       在django框架中信号量意思是达到某个条件自动触发特定功能

自定义互斥锁

自定义互斥锁可以保证相关数据安全,但是效率变低了

from threading import Thread,Lock
import time

mutex=Lock()
n=100
def task():
    global n
    with mutex:
        temp=n
        time.sleep(0.1)
        n=temp-1

if __name__ == '__main__':
    l=[]
    for i in range(100):
        t=Thread(target=task)
        l.append(t)
        t.start()

    for t in l:
        t.join()
    print(n)

结果:
0

**如果将自定义互斥锁比喻成是单个厕所(一个坑位)

那么信号量相当于是公共厕所(多个坑位)**

python里面的信号量semaphore

python统一了所有的命名,使用与线程锁(互斥锁)同样的方法命名消耗和释放资源
    acquire方法 消耗资源加1 空车位减1
    release方法 释放资源加1 空车位加1
    创建Semaphore类实例才可以使用信号量semaphore
    通过该类的构造方法传入计数器的最大值空车位总数


from threading import Semaphore
    Max =3
    s =Semaphore(Max)
    print(s._value)#输出计数器的值
    s.acquire()#消耗1个资源
    s.acquire()
    s.acquire()
    print(s._value)#输出计数器的值   这时输出的是0 也可以表示为False   当设定条件时,可以使用False来作为条件判断
    # s.acquire()#已经没有资源了,再减就一直处于等待状态,除非设定了其他资源在执行完毕后并释放  这样才能继续消耗

    s.release()#释放1个资源
    s.release()
    s.release()
    # s.release()#已超过资源设定的最大值了,再加就抛出异常    相当于停车场一共才3个车位,怎么会显示有4个车位呢?
    print(s._value)#输出计数器的值


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几种方法

event.isSet():返回event的状态值;

event.wait():如果 event.isSet()==False将阻塞线程;

event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;

event.clear():恢复event的状态值为False。

例红绿灯

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服务端实现并发

多进程:来一个客户端就开一个进程(临时工)
多线程:来一个客户端就开一个线程(临时工)

计算机硬件是有物理极限的 我们不可能无限制的创建进程和线程

措施

# 池是用来保证计算机硬件安全的情况下最大限度的利用计算机
它降低了程序的运行效率但是保证了计算机硬件的安全从而让你写的程序能够正常运行

进程池

提前创建好固定数量的进程 后续反复使用这些进程(合同工)

线程池

提前创建好固定数量的线程 后续反复使用这些线程(合同工)
如果任务超出了池子里面的最大进程或线程数 则原地等待

进程池和线程池其实降低了程序的运行效率 但是保证了硬件的安全

代码演示(掌握)

进程池和线程池的使用方法完全一致,只是它们的类名不同而已,另外要注意区分任务类型,对于IO多计算 少的任务使用线程池,对于计算多IO少的任务使用进程池。

# concurrent.futures模块是CPython官方提供的进程池/线程池模块,ThreadPoolExecutor类封装了线程池的相关方法、ProcessPoolExecutor类封装了进程池的相关方法。


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)

协程是协调个部分代码达到资源最大利用,这才是真正的协程,在协程中要有任务的安排调整

协程就是这样发生在一个可能发生长时间阻塞的地方,我们不是让CPU做无用的等待,而是让CPU在等待的 时间干点其他有用的事情,我们手动进行任务切换的过程就是协程。

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

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

首先需要强调的是协程完全是程序员自己意想出来的名词!
对于操作系统而言之认识进程和线程
协程就是自己通过代码来检测程序的IO操作并自己处理 让CPU感觉不到IO的存在从而最大幅度的占用CPU

类似于一个人同时干接待和服务客人的活 在接待与服务之间来回切换!!!
"""

gevent的介绍

greenlet已经实现了协程,但是这个还要人工切换,这里介绍一个比greenlet更强大而且能够自动切换任务的第三方库,那就是gevent。

    gevent内部封装的greenlet,其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。

    由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。

安装gevent

pip3 install gevent

给程序打补丁

from gevent import monkey
# 打补丁,让gevent框架识别耗时操作,比如:time.sleep,网络请求延时
monkey.patch_all()

基本使用

# 保存的功能 我们其实接触过  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密集型的

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